Skip to content

Commit 101ae63

Browse files
committed
Don't coerce non-String inputs to strings
This commit stops converting non-string inputs to strings when parsing ISO strings or property bag fields. When numbers are coerced to strings, the result is sometimes a valid ISO string subset but it often doesn't behave as expected. The result is often brittle, yielding "data driven exceptions" that we've tried to avoid. Numbers can also be ambiguous, e.g. is a Number passed for `offset` mean hours like an ISO string, minutes like Date.p.getTimezoneOffset, or msecs like Date.p.getTime(). This commit also removes coercing objects to strings in the same contexts (like TimeZone and Calendar constructors) because it's unclear that there are use cases for this coercion.
1 parent 736d6db commit 101ae63

15 files changed

+86
-69
lines changed

polyfill/lib/calendar.mjs

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,14 @@ const impl = {};
4141

4242
export class Calendar {
4343
constructor(id) {
44-
// Note: if the argument is not passed, IsBuiltinCalendar("undefined") will fail. This check
45-
// exists only to improve the error message.
46-
if (arguments.length < 1) {
47-
throw new RangeError('missing argument: id is required');
48-
}
49-
50-
id = ES.ToString(id);
51-
if (!ES.IsBuiltinCalendar(id)) throw new RangeError(`invalid calendar identifier ${id}`);
44+
const stringId = ES.RequireString(id);
45+
if (!ES.IsBuiltinCalendar(stringId)) throw new RangeError(`invalid calendar identifier ${stringId}`);
5246
CreateSlots(this);
53-
SetSlot(this, CALENDAR_ID, ES.ASCIILowercase(id));
47+
SetSlot(this, CALENDAR_ID, ES.ASCIILowercase(stringId));
5448

5549
if (typeof __debug__ !== 'undefined' && __debug__) {
5650
Object.defineProperty(this, '_repr_', {
57-
value: `${this[Symbol.toStringTag]} <${id}>`,
51+
value: `${this[Symbol.toStringTag]} <${stringId}>`,
5852
writable: false,
5953
enumerable: false,
6054
configurable: false

polyfill/lib/ecmascript.mjs

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const ObjectCreate = Object.create;
1818
const ObjectDefineProperty = Object.defineProperty;
1919
const ObjectEntries = Object.entries;
2020
const ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
21+
const StringCtor = String;
2122
const StringFromCharCode = String.fromCharCode;
2223
const StringPrototypeCharCodeAt = String.prototype.charCodeAt;
2324
const StringPrototypeMatchAll = String.prototype.matchAll;
@@ -146,10 +147,27 @@ export function ToIntegerIfIntegral(value) {
146147
return number;
147148
}
148149

150+
// This convenience function isn't in the spec, but is useful in the polyfill
151+
// for DRY and better error messages.
152+
export function RequireString(value) {
153+
if (Type(value) !== 'String') {
154+
// Use String() to ensure that Symbols won't throw
155+
throw new TypeError(`expected a string, not ${StringCtor(value)}`);
156+
}
157+
return value;
158+
}
159+
160+
// This function is an enum in the spec, but it's helpful to make it a
161+
// function in the polyfill.
162+
function ToPrimitiveAndRequireString(value) {
163+
value = ToPrimitive(value, StringCtor);
164+
return RequireString(value);
165+
}
166+
149167
const BUILTIN_CASTS = new Map([
150168
['year', ToIntegerWithTruncation],
151169
['month', ToPositiveIntegerWithTruncation],
152-
['monthCode', ToString],
170+
['monthCode', ToPrimitiveAndRequireString],
153171
['day', ToPositiveIntegerWithTruncation],
154172
['hour', ToIntegerWithTruncation],
155173
['minute', ToIntegerWithTruncation],
@@ -167,9 +185,9 @@ const BUILTIN_CASTS = new Map([
167185
['milliseconds', ToIntegerIfIntegral],
168186
['microseconds', ToIntegerIfIntegral],
169187
['nanoseconds', ToIntegerIfIntegral],
170-
['era', ToString],
188+
['era', ToPrimitiveAndRequireString],
171189
['eraYear', ToIntegerOrInfinity],
172-
['offset', ToString]
190+
['offset', ToPrimitiveAndRequireString]
173191
]);
174192

175193
const BUILTIN_DEFAULTS = new Map([
@@ -698,7 +716,7 @@ export function RegulateISOYearMonth(year, month, overflow) {
698716

699717
export function ToTemporalDurationRecord(item) {
700718
if (Type(item) !== 'Object') {
701-
return ParseTemporalDurationString(ToString(item));
719+
return ParseTemporalDurationString(RequireString(item));
702720
}
703721
if (IsTemporalDuration(item)) {
704722
return {
@@ -980,7 +998,7 @@ export function ToRelativeTemporalObject(options) {
980998
} else {
981999
let tzName, z;
9821000
({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar, tzName, offset, z } =
983-
ParseISODateTime(ToString(relativeTo)));
1001+
ParseISODateTime(RequireString(relativeTo)));
9841002
if (tzName) {
9851003
timeZone = ToTemporalTimeZoneSlotValue(tzName);
9861004
if (z) {
@@ -1128,7 +1146,7 @@ export function ToTemporalDate(item, options) {
11281146
return CalendarDateFromFields(calendar, fields, options);
11291147
}
11301148
ToTemporalOverflow(options); // validate and ignore
1131-
let { year, month, day, calendar, z } = ParseTemporalDateString(ToString(item));
1149+
let { year, month, day, calendar, z } = ParseTemporalDateString(RequireString(item));
11321150
if (z) throw new RangeError('Z designator not supported for PlainDate');
11331151
if (!calendar) calendar = 'iso8601';
11341152
if (!IsBuiltinCalendar(calendar)) throw new RangeError(`invalid calendar identifier ${calendar}`);
@@ -1202,7 +1220,7 @@ export function ToTemporalDateTime(item, options) {
12021220
ToTemporalOverflow(options); // validate and ignore
12031221
let z;
12041222
({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar, z } =
1205-
ParseTemporalDateTimeString(ToString(item)));
1223+
ParseTemporalDateTimeString(RequireString(item)));
12061224
if (z) throw new RangeError('Z designator not supported for PlainDateTime');
12071225
RejectDateTime(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond);
12081226
if (!calendar) calendar = 'iso8601';
@@ -1237,7 +1255,8 @@ export function ToTemporalInstant(item) {
12371255
const TemporalInstant = GetIntrinsic('%Temporal.Instant%');
12381256
return new TemporalInstant(GetSlot(item, EPOCHNANOSECONDS));
12391257
}
1240-
const ns = ParseTemporalInstant(ToString(item));
1258+
item = ToPrimitive(item, StringCtor);
1259+
const ns = ParseTemporalInstant(RequireString(item));
12411260
const TemporalInstant = GetIntrinsic('%Temporal.Instant%');
12421261
return new TemporalInstant(ns);
12431262
}
@@ -1267,7 +1286,7 @@ export function ToTemporalMonthDay(item, options) {
12671286
}
12681287

12691288
ToTemporalOverflow(options); // validate and ignore
1270-
let { month, day, referenceISOYear, calendar } = ParseTemporalMonthDayString(ToString(item));
1289+
let { month, day, referenceISOYear, calendar } = ParseTemporalMonthDayString(RequireString(item));
12711290
if (calendar === undefined) calendar = 'iso8601';
12721291
if (!IsBuiltinCalendar(calendar)) throw new RangeError(`invalid calendar identifier ${calendar}`);
12731292
calendar = ASCIILowercase(calendar);
@@ -1309,7 +1328,7 @@ export function ToTemporalTime(item, overflow = 'constrain') {
13091328
overflow
13101329
));
13111330
} else {
1312-
({ hour, minute, second, millisecond, microsecond, nanosecond } = ParseTemporalTimeString(ToString(item)));
1331+
({ hour, minute, second, millisecond, microsecond, nanosecond } = ParseTemporalTimeString(RequireString(item)));
13131332
RejectTime(hour, minute, second, millisecond, microsecond, nanosecond);
13141333
}
13151334
const TemporalPlainTime = GetIntrinsic('%Temporal.PlainTime%');
@@ -1326,7 +1345,7 @@ export function ToTemporalYearMonth(item, options) {
13261345
}
13271346

13281347
ToTemporalOverflow(options); // validate and ignore
1329-
let { year, month, referenceISODay, calendar } = ParseTemporalYearMonthString(ToString(item));
1348+
let { year, month, referenceISODay, calendar } = ParseTemporalYearMonthString(RequireString(item));
13301349
if (calendar === undefined) calendar = 'iso8601';
13311350
if (!IsBuiltinCalendar(calendar)) throw new RangeError(`invalid calendar identifier ${calendar}`);
13321351
calendar = ASCIILowercase(calendar);
@@ -1447,7 +1466,7 @@ export function ToTemporalZonedDateTime(item, options) {
14471466
} else {
14481467
let tzName, z;
14491468
({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, tzName, offset, z, calendar } =
1450-
ParseTemporalZonedDateTimeString(ToString(item)));
1469+
ParseTemporalZonedDateTimeString(RequireString(item)));
14511470
timeZone = ToTemporalTimeZoneSlotValue(tzName);
14521471
if (z) {
14531472
offsetBehaviour = 'exact';
@@ -1973,7 +1992,7 @@ export function ToTemporalCalendarSlotValue(calendarLike) {
19731992
}
19741993
return calendarLike;
19751994
}
1976-
const identifier = ToString(calendarLike);
1995+
const identifier = RequireString(calendarLike);
19771996
if (IsBuiltinCalendar(identifier)) return ASCIILowercase(identifier);
19781997
let calendar;
19791998
try {
@@ -2092,7 +2111,7 @@ export function ToTemporalTimeZoneSlotValue(temporalTimeZoneLike) {
20922111
}
20932112
return temporalTimeZoneLike;
20942113
}
2095-
const identifier = ToString(temporalTimeZoneLike);
2114+
const identifier = RequireString(temporalTimeZoneLike);
20962115
const { tzName, offset, z } = ParseTemporalTimeZoneString(identifier);
20972116
if (tzName) {
20982117
// tzName is any valid identifier string in brackets, and could be an offset identifier

polyfill/lib/timezone.mjs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,7 @@ import {
2121

2222
export class TimeZone {
2323
constructor(identifier) {
24-
// Note: if the argument is not passed, GetCanonicalTimeZoneIdentifier(undefined) will throw.
25-
// This check exists only to improve the error message.
26-
if (arguments.length < 1) {
27-
throw new RangeError('missing argument: identifier is required');
28-
}
29-
let stringIdentifier = ES.ToString(identifier);
24+
let stringIdentifier = ES.RequireString(identifier);
3025
const parseResult = ES.ParseTimeZoneIdentifier(identifier);
3126
if (parseResult.offsetNanoseconds !== undefined) {
3227
stringIdentifier = ES.FormatOffsetTimeZoneIdentifier(parseResult.offsetNanoseconds);

polyfill/test262

Submodule test262 updated 1034 files

spec/abstractops.html

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -567,16 +567,21 @@ <h1>
567567
</emu-table>
568568
</emu-clause>
569569

570-
<emu-clause id="sec-temporal-torelativetemporalobject" aoid="ToRelativeTemporalObject">
571-
<h1>ToRelativeTemporalObject ( _options_ )</h1>
572-
<p>
573-
The abstract operation ToRelativeTemporalObject examines the value of the `relativeTo` property of its _options_ argument.
574-
If this is not present, it returns *undefined*.
575-
Otherwise, it attempts to return a Temporal.ZonedDateTime instance or Temporal.PlainDate instance, in order of preference, by converting the value.
576-
If neither of those are possible, the operation throws a *RangeError*.
577-
</p>
570+
<emu-clause id="sec-temporal-torelativetemporalobject" type="abstract operation">
571+
<h1>ToRelativeTemporalObject (
572+
_options_: an Object
573+
): either a normal completion containing either a Temporal.ZonedDateTime object or a Temporal.PlainDate object, or a throw completion</h1>
574+
<dl class="header">
575+
<dt>description</dt>
576+
<dd>
577+
It examines the value of the `relativeTo` property of its _options_ argument.
578+
If the value is *undefined*, it returns *undefined*.
579+
If the value is not a String or an Object, it throws a *TypeError*.
580+
Otherwise, it attempts to return a Temporal.ZonedDateTime instance or Temporal.PlainDate instance, in order of preference, by converting the value.
581+
If neither of those are possible, it throws a *RangeError*.
582+
</dd>
583+
</dl>
578584
<emu-alg>
579-
1. Assert: Type(_options_) is Object.
580585
1. Let _value_ be ? Get(_options_, *"relativeTo"*).
581586
1. If _value_ is *undefined*, return *undefined*.
582587
1. Let _offsetBehaviour_ be ~option~.
@@ -601,8 +606,8 @@ <h1>ToRelativeTemporalObject ( _options_ )</h1>
601606
1. If _offsetString_ is *undefined*, then
602607
1. Set _offsetBehaviour_ to ~wall~.
603608
1. Else,
604-
1. Let _string_ be ? ToString(_value_).
605-
1. Let _result_ be ? ParseTemporalRelativeToString(_string_).
609+
1. If _value_ is not a String, throw a *TypeError* exception.
610+
1. Let _result_ be ? ParseTemporalRelativeToString(_value_).
606611
1. Let _offsetString_ be _result_.[[TimeZone]].[[OffsetString]].
607612
1. Let _timeZoneName_ be _result_.[[TimeZone]].[[Name]].
608613
1. If _timeZoneName_ is *undefined*, then
@@ -1825,8 +1830,10 @@ <h1>
18251830
1. Set _value_ to ? ToPositiveIntegerWithTruncation(_value_).
18261831
1. Set _value_ to 𝔽(_value_).
18271832
1. Else,
1828-
1. Assert: _Conversion_ is ~ToString~.
1829-
1. Set _value_ to ? ToString(_value_).
1833+
1. Assert: _Conversion_ is ~ToPrimitiveAndRequireString~.
1834+
1. NOTE: Non-primitive values are supported here for consistency with other fields, but such values must coerce to Strings.
1835+
1. Set _value_ to ? ToPrimitive(_value_, ~string~).
1836+
1. If _value_ is not a String, throw a *TypeError* exception.
18301837
1. Perform ! CreateDataPropertyOrThrow(_result_, _property_, _value_).
18311838
1. Else if _requiredFields_ is a List, then
18321839
1. If _requiredFields_ contains _property_, then
@@ -1861,7 +1868,7 @@ <h1>
18611868
</tr>
18621869
<tr>
18631870
<td>*"monthCode"*</td>
1864-
<td>~ToString~</td>
1871+
<td>~ToPrimitiveAndRequireString~</td>
18651872
<td>*undefined*</td>
18661873
</tr>
18671874
<tr>
@@ -1901,12 +1908,12 @@ <h1>
19011908
</tr>
19021909
<tr>
19031910
<td>*"offset"*</td>
1904-
<td>~ToString~</td>
1911+
<td>~ToPrimitiveAndRequireString~</td>
19051912
<td>*undefined*</td>
19061913
</tr>
19071914
<tr>
19081915
<td>*"era"*</td>
1909-
<td>~ToString~</td>
1916+
<td>~ToPrimitiveAndRequireString~</td>
19101917
<td>*undefined*</td>
19111918
</tr>
19121919
<tr>

spec/calendar.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -525,8 +525,8 @@ <h1>
525525
1. Return _temporalCalendarLike_.[[Calendar]].
526526
1. If ? ObjectImplementsTemporalCalendarProtocol(_temporalCalendarLike_) is *false*, throw a *TypeError* exception.
527527
1. Return _temporalCalendarLike_.
528-
1. Let _identifier_ be ? ToString(_temporalCalendarLike_).
529-
1. Set _identifier_ to ? ParseTemporalCalendarString(_identifier_).
528+
1. If _temporalCalendarLike_ is not a String, throw a *TypeError* exception.
529+
1. Let _identifier_ be ? ParseTemporalCalendarString(_temporalCalendarLike_).
530530
1. If IsBuiltinCalendar(_identifier_) is *false*, throw a *RangeError* exception.
531531
1. Return the ASCII-lowercase of _identifier_.
532532
</emu-alg>
@@ -1020,7 +1020,7 @@ <h1>Temporal.Calendar ( _id_ )</h1>
10201020
<emu-alg>
10211021
1. If NewTarget is *undefined*, then
10221022
1. Throw a *TypeError* exception.
1023-
1. Set _id_ to ? ToString(_id_).
1023+
1. If _id_ is not a String, throw a *TypeError* exception.
10241024
1. If IsBuiltinCalendar(_id_) is *false*, then
10251025
1. Throw a *RangeError* exception.
10261026
1. Return ? CreateTemporalCalendar(_id_, NewTarget).

spec/duration.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -912,8 +912,8 @@ <h1>
912912
</dl>
913913
<emu-alg>
914914
1. If Type(_temporalDurationLike_) is not Object, then
915-
1. Let _string_ be ? ToString(_temporalDurationLike_).
916-
1. Return ? ParseTemporalDurationString(_string_).
915+
1. If _temporalDurationLike_ is not a String, throw a *TypeError* exception.
916+
1. Return ? ParseTemporalDurationString(_temporalDurationLike_).
917917
1. If _temporalDurationLike_ has an [[InitializedTemporalDuration]] internal slot, then
918918
1. Return ! CreateDurationRecord(_temporalDurationLike_.[[Years]], _temporalDurationLike_.[[Months]], _temporalDurationLike_.[[Weeks]], _temporalDurationLike_.[[Days]], _temporalDurationLike_.[[Hours]], _temporalDurationLike_.[[Minutes]], _temporalDurationLike_.[[Seconds]], _temporalDurationLike_.[[Milliseconds]], _temporalDurationLike_.[[Microseconds]], _temporalDurationLike_.[[Nanoseconds]]).
919919
1. Let _result_ be a new Duration Record with each field set to 0.

spec/instant.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -518,8 +518,10 @@ <h1>ToTemporalInstant ( _item_ )</h1>
518518
1. Return _item_.
519519
1. If _item_ has an [[InitializedTemporalZonedDateTime]] internal slot, then
520520
1. Return ! CreateTemporalInstant(_item_.[[Nanoseconds]]).
521-
1. Let _string_ be ? ToString(_item_).
522-
1. Let _epochNanoseconds_ be ? ParseTemporalInstant(_string_).
521+
1. NOTE: This use of ToPrimitive allows Instant-like objects to be converted.
522+
1. Set _item_ to ? ToPrimitive(_item_, ~string~).
523+
1. If _item_ is not a String, throw a *TypeError* exception.
524+
1. Let _epochNanoseconds_ be ? ParseTemporalInstant(_item_).
523525
1. Return ! CreateTemporalInstant(_epochNanoseconds_).
524526
</emu-alg>
525527
</emu-clause>

spec/plaindate.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -756,8 +756,8 @@ <h1>ToTemporalDate ( _item_ [ , _options_ ] )</h1>
756756
1. Let _fields_ be ? PrepareTemporalFields(_item_, _fieldNames_, «»).
757757
1. Return ? CalendarDateFromFields(_calendar_, _fields_, _options_).
758758
1. Perform ? ToTemporalOverflow(_options_).
759-
1. Let _string_ be ? ToString(_item_).
760-
1. Let _result_ be ? ParseTemporalDateString(_string_).
759+
1. If _item_ is not a String, throw a *TypeError* exception.
760+
1. Let _result_ be ? ParseTemporalDateString(_item_).
761761
1. Assert: IsValidISODate(_result_.[[Year]], _result_.[[Month]], _result_.[[Day]]) is *true*.
762762
1. Let _calendar_ be _result_.[[Calendar]].
763763
1. If _calendar_ is *undefined*, set _calendar_ to *"iso8601"*.

spec/plaindatetime.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -916,8 +916,8 @@ <h1>ToTemporalDateTime ( _item_ [ , _options_ ] )</h1>
916916
1. Let _result_ be ? InterpretTemporalDateTimeFields(_calendar_, _fields_, _options_).
917917
1. Else,
918918
1. Perform ? ToTemporalOverflow(_options_).
919-
1. Let _string_ be ? ToString(_item_).
920-
1. Let _result_ be ? ParseTemporalDateTimeString(_string_).
919+
1. If _item_ is not a String, throw a *TypeError* exception.
920+
1. Let _result_ be ? ParseTemporalDateTimeString(_item_).
921921
1. Assert: IsValidISODate(_result_.[[Year]], _result_.[[Month]], _result_.[[Day]]) is *true*.
922922
1. Assert: IsValidTime(_result_.[[Hour]], _result_.[[Minute]], _result_.[[Second]], _result_.[[Millisecond]], _result_.[[Microsecond]], _result_.[[Nanosecond]]) is *true*.
923923
1. Let _calendar_ be _result_.[[Calendar]].

0 commit comments

Comments
 (0)