[INTL] Intl Constructors not web compatible with Object.create usage
authorcommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 10 Mar 2016 05:28:34 +0000 (05:28 +0000)
committercommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 10 Mar 2016 05:28:34 +0000 (05:28 +0000)
https://bugs.webkit.org/show_bug.cgi?id=153679

Patch by Andy VanWagoner <thetalecrafter@gmail.com> on 2016-03-09
Reviewed by Darin Adler.

Source/JavaScriptCore:

Add workaround for initializing NumberFormat and DateTimeFormat objects
using Object.create followed by constructor.call. This is necessary for
backwards compatibility with libraries relying on v1 behavior of Intl
constructors.

Collator does not get the workaround, since polyfills do not include it,
and there are not any known instances of v2 incompatible libraries.

The workaround involves checking for an object that inherits from the
*Format constructor, but was not actually initialized with that type. A
substitute instance is created and attached to the object using a private
name. The prototype functions then check for the private property to use
in place of the original object.

Since this behavior is not part of the v2 spec, it should be removed as
soon as the incompatible behavior is no longer in common use.

* runtime/CommonIdentifiers.h:
* runtime/IntlDateTimeFormatConstructor.cpp:
(JSC::callIntlDateTimeFormat):
* runtime/IntlDateTimeFormatPrototype.cpp:
(JSC::IntlDateTimeFormatPrototypeGetterFormat):
(JSC::IntlDateTimeFormatPrototypeFuncResolvedOptions):
* runtime/IntlNumberFormatConstructor.cpp:
(JSC::callIntlNumberFormat):
* runtime/IntlNumberFormatPrototype.cpp:
(JSC::IntlNumberFormatPrototypeGetterFormat):
(JSC::IntlNumberFormatPrototypeFuncResolvedOptions):

LayoutTests:

Add tests for Object.create + contructor.call initialization of NumberFormat
and DateTimeFormat objects.

* js/intl-datetimeformat-expected.txt:
* js/intl-numberformat-expected.txt:
* js/script-tests/intl-datetimeformat.js:
* js/script-tests/intl-numberformat.js:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@197925 268f45cc-cd09-0410-ab3c-d52691b4dbfc

LayoutTests/ChangeLog
LayoutTests/js/intl-datetimeformat-expected.txt
LayoutTests/js/intl-numberformat-expected.txt
LayoutTests/js/script-tests/intl-datetimeformat.js
LayoutTests/js/script-tests/intl-numberformat.js
Source/JavaScriptCore/ChangeLog
Source/JavaScriptCore/runtime/CommonIdentifiers.h
Source/JavaScriptCore/runtime/IntlDateTimeFormatConstructor.cpp
Source/JavaScriptCore/runtime/IntlDateTimeFormatPrototype.cpp
Source/JavaScriptCore/runtime/IntlNumberFormatConstructor.cpp
Source/JavaScriptCore/runtime/IntlNumberFormatPrototype.cpp

index 23cfe53..8ed3e0c 100644 (file)
@@ -1,3 +1,18 @@
+2016-03-09  Andy VanWagoner  <thetalecrafter@gmail.com>
+
+        [INTL] Intl Constructors not web compatible with Object.create usage
+        https://bugs.webkit.org/show_bug.cgi?id=153679
+
+        Reviewed by Darin Adler.
+
+        Add tests for Object.create + contructor.call initialization of NumberFormat
+        and DateTimeFormat objects.
+
+        * js/intl-datetimeformat-expected.txt:
+        * js/intl-numberformat-expected.txt:
+        * js/script-tests/intl-datetimeformat.js:
+        * js/script-tests/intl-numberformat.js:
+
 2016-03-09  Ryosuke Niwa  <rniwa@webkit.org>
 
         defineElement should upgrade existing unresolved custom elements
index f58d4a9..64486ea 100644 (file)
@@ -2147,6 +2147,9 @@ PASS
     var resolved = Intl.DateTimeFormat("zh-TW", options).resolvedOptions();
     Object.keys(options).every(option => resolved[option] != null) is true
 PASS typeof Intl.DateTimeFormat("zh-TW", { hour: "numeric", minute: "numeric" }).format() === "string" is true
+PASS var legacy = Object.create(Intl.DateTimeFormat.prototype);Intl.DateTimeFormat.apply(legacy) is legacy
+PASS var legacy = Object.create(Intl.DateTimeFormat.prototype);Intl.DateTimeFormat.call(legacy, 'en-u-nu-arab', { timeZone: 'America/Los_Angeles' }).format(1451099872641) is '١٢/٢٥/٢٠١٥'
+PASS var incompat = {};Intl.DateTimeFormat.apply(incompat) is not incompat
 PASS successfullyParsed is true
 
 TEST COMPLETE
index df8cb8e..26bdc33 100644 (file)
@@ -194,6 +194,9 @@ PASS Intl.NumberFormat.prototype.resolvedOptions() is an instance of Object
 PASS Intl.NumberFormat.prototype.resolvedOptions() === Intl.NumberFormat.prototype.resolvedOptions() is false
 PASS Intl.NumberFormat.prototype.resolvedOptions.call(5) threw exception TypeError: Intl.NumberFormat.prototype.resolvedOptions called on value that's not an object initialized as a NumberFormat.
 PASS var options = Intl.NumberFormat.prototype.resolvedOptions(); delete options['locale']; JSON.stringify(options) is '{"numberingSystem":"latn","style":"decimal","minimumIntegerDigits":1,"minimumFractionDigits":0,"maximumFractionDigits":3,"useGrouping":true}'
+PASS var legacy = Object.create(Intl.NumberFormat.prototype);Intl.NumberFormat.apply(legacy) is legacy
+PASS var legacy = Object.create(Intl.NumberFormat.prototype);Intl.NumberFormat.call(legacy, 'en-u-nu-arab').format(1.2345) is '١٫٢٣٥'
+PASS var incompat = {};Intl.NumberFormat.apply(incompat) is not incompat
 PASS successfullyParsed is true
 
 TEST COMPLETE
index 927a930..fbf890a 100644 (file)
@@ -497,3 +497,9 @@ for (let locale of localesSample) {
     Object.keys(options).every(option => resolved[option] != null)`);
   shouldBeTrue(`typeof Intl.DateTimeFormat("${locale}", { hour: "numeric", minute: "numeric" }).format() === "string"`);
 }
+
+// Legacy compatibility with ECMA-402 1.0
+let legacyInit = "var legacy = Object.create(Intl.DateTimeFormat.prototype);";
+shouldBe(legacyInit + "Intl.DateTimeFormat.apply(legacy)", "legacy");
+shouldBe(legacyInit + "Intl.DateTimeFormat.call(legacy, 'en-u-nu-arab', { timeZone: 'America/Los_Angeles' }).format(1451099872641)", "'١٢/٢٥/٢٠١٥'");
+shouldNotBe("var incompat = {};Intl.DateTimeFormat.apply(incompat)", "incompat");
index 8127944..7617115 100644 (file)
@@ -338,3 +338,9 @@ shouldThrow("Intl.NumberFormat.prototype.resolvedOptions.call(5)", "'TypeError:
 
 // Returns the default options.
 shouldBe("var options = Intl.NumberFormat.prototype.resolvedOptions(); delete options['locale']; JSON.stringify(options)", '\'{"numberingSystem":"latn","style":"decimal","minimumIntegerDigits":1,"minimumFractionDigits":0,"maximumFractionDigits":3,"useGrouping":true}\'');
+
+// Legacy compatibility with ECMA-402 1.0
+let legacyInit = "var legacy = Object.create(Intl.NumberFormat.prototype);";
+shouldBe(legacyInit + "Intl.NumberFormat.apply(legacy)", "legacy");
+shouldBe(legacyInit + "Intl.NumberFormat.call(legacy, 'en-u-nu-arab').format(1.2345)", "'١٫٢٣٥'");
+shouldNotBe("var incompat = {};Intl.NumberFormat.apply(incompat)", "incompat");
index 3fd8ffe..964053d 100644 (file)
@@ -1,3 +1,39 @@
+2016-03-09  Andy VanWagoner  <thetalecrafter@gmail.com>
+
+        [INTL] Intl Constructors not web compatible with Object.create usage
+        https://bugs.webkit.org/show_bug.cgi?id=153679
+
+        Reviewed by Darin Adler.
+
+        Add workaround for initializing NumberFormat and DateTimeFormat objects
+        using Object.create followed by constructor.call. This is necessary for
+        backwards compatibility with libraries relying on v1 behavior of Intl
+        constructors.
+
+        Collator does not get the workaround, since polyfills do not include it,
+        and there are not any known instances of v2 incompatible libraries.
+
+        The workaround involves checking for an object that inherits from the
+        *Format constructor, but was not actually initialized with that type. A
+        substitute instance is created and attached to the object using a private
+        name. The prototype functions then check for the private property to use
+        in place of the original object.
+
+        Since this behavior is not part of the v2 spec, it should be removed as
+        soon as the incompatible behavior is no longer in common use.
+
+        * runtime/CommonIdentifiers.h:
+        * runtime/IntlDateTimeFormatConstructor.cpp:
+        (JSC::callIntlDateTimeFormat):
+        * runtime/IntlDateTimeFormatPrototype.cpp:
+        (JSC::IntlDateTimeFormatPrototypeGetterFormat):
+        (JSC::IntlDateTimeFormatPrototypeFuncResolvedOptions):
+        * runtime/IntlNumberFormatConstructor.cpp:
+        (JSC::callIntlNumberFormat):
+        * runtime/IntlNumberFormatPrototype.cpp:
+        (JSC::IntlNumberFormatPrototypeGetterFormat):
+        (JSC::IntlNumberFormatPrototypeFuncResolvedOptions):
+
 2016-03-09  Saam barati  <sbarati@apple.com>
 
         Add proper JSON.stringify support for Proxy when the target is an array
index 6c04359..2695e48 100644 (file)
     macro(Collator) \
     macro(DateTimeFormat) \
     macro(NumberFormat) \
+    macro(intlSubstituteValue) \
     macro(thisTimeValue) \
     macro(newTargetLocal) \
     macro(derivedConstructor) \
index e917ee2..5b0e5f0 100644 (file)
@@ -117,17 +117,35 @@ static EncodedJSValue JSC_HOST_CALL callIntlDateTimeFormat(ExecState* state)
     // 1. If NewTarget is undefined, let newTarget be the active function object, else let newTarget be NewTarget.
     // NewTarget is always undefined when called as a function.
 
-    // 2. Let dateTimeFormat be OrdinaryCreateFromConstructor(newTarget, %DateTimeFormatPrototype%).
     VM& vm = state->vm();
-    IntlDateTimeFormat* dateTimeFormat = IntlDateTimeFormat::create(vm, jsCast<IntlDateTimeFormatConstructor*>(state->callee()));
+    IntlDateTimeFormatConstructor* callee = jsCast<IntlDateTimeFormatConstructor*>(state->callee());
+
+    // FIXME: Workaround to provide compatibility with ECMA-402 1.0 call/apply patterns.
+    JSValue thisValue = state->thisValue();
+    IntlDateTimeFormat* dateTimeFormat = jsDynamicCast<IntlDateTimeFormat*>(thisValue);
+    if (!dateTimeFormat) {
+        JSValue prototype = callee->getDirect(vm, vm.propertyNames->prototype);
+        if (JSObject::defaultHasInstance(state, thisValue, prototype)) {
+            JSObject* thisObject = thisValue.toObject(state);
+            if (state->hadException())
+                return JSValue::encode(jsUndefined());
+
+            dateTimeFormat = IntlDateTimeFormat::create(vm, callee);
+            dateTimeFormat->initializeDateTimeFormat(*state, state->argument(0), state->argument(1));
+            if (state->hadException())
+                return JSValue::encode(jsUndefined());
+
+            thisObject->putDirect(vm, vm.propertyNames->intlSubstituteValuePrivateName, dateTimeFormat);
+            return JSValue::encode(thisValue);
+        }
+    }
 
+    // 2. Let dateTimeFormat be OrdinaryCreateFromConstructor(newTarget, %DateTimeFormatPrototype%).
     // 3. ReturnIfAbrupt(dateTimeFormat).
-    ASSERT(dateTimeFormat);
+    dateTimeFormat = IntlDateTimeFormat::create(vm, callee);
 
     // 4. Return InitializeDateTimeFormat(dateTimeFormat, locales, options).
-    JSValue locales = state->argument(0);
-    JSValue options = state->argument(1);
-    dateTimeFormat->initializeDateTimeFormat(*state, locales, options);
+    dateTimeFormat->initializeDateTimeFormat(*state, state->argument(0), state->argument(1));
     return JSValue::encode(dateTimeFormat);
 }
 
index d2c54d6..e683515 100644 (file)
@@ -117,6 +117,11 @@ EncodedJSValue JSC_HOST_CALL IntlDateTimeFormatPrototypeGetterFormat(ExecState*
     // 12.3.3 Intl.DateTimeFormat.prototype.format (ECMA-402 2.0)
     // 1. Let dtf be this DateTimeFormat object.
     IntlDateTimeFormat* dtf = jsDynamicCast<IntlDateTimeFormat*>(state->thisValue());
+
+    // FIXME: Workaround to provide compatibility with ECMA-402 1.0 call/apply patterns.
+    if (!dtf)
+        dtf = jsDynamicCast<IntlDateTimeFormat*>(state->thisValue().get(state, state->vm().propertyNames->intlSubstituteValuePrivateName));
+
     // 2. ReturnIfAbrupt(dtf).
     if (!dtf)
         return JSValue::encode(throwTypeError(state, ASCIILiteral("Intl.DateTimeFormat.prototype.format called on value that's not an object initialized as a DateTimeFormat")));
@@ -148,6 +153,11 @@ EncodedJSValue JSC_HOST_CALL IntlDateTimeFormatPrototypeFuncResolvedOptions(Exec
 {
     // 12.3.5 Intl.DateTimeFormat.prototype.resolvedOptions() (ECMA-402 2.0)
     IntlDateTimeFormat* dateTimeFormat = jsDynamicCast<IntlDateTimeFormat*>(state->thisValue());
+
+    // FIXME: Workaround to provide compatibility with ECMA-402 1.0 call/apply patterns.
+    if (!dateTimeFormat)
+        dateTimeFormat = jsDynamicCast<IntlDateTimeFormat*>(state->thisValue().get(state, state->vm().propertyNames->intlSubstituteValuePrivateName));
+
     if (!dateTimeFormat)
         return JSValue::encode(throwTypeError(state, ASCIILiteral("Intl.DateTimeFormat.prototype.resolvedOptions called on value that's not an object initialized as a DateTimeFormat")));
 
index 26c6d46..0a11078 100644 (file)
@@ -117,17 +117,35 @@ static EncodedJSValue JSC_HOST_CALL callIntlNumberFormat(ExecState* state)
     // 1. If NewTarget is undefined, let newTarget be the active function object, else let newTarget be NewTarget.
     // NewTarget is always undefined when called as a function.
 
-    // 2. Let numberFormat be OrdinaryCreateFromConstructor(newTarget, %NumberFormatPrototype%).
+    // FIXME: Workaround to provide compatibility with ECMA-402 1.0 call/apply patterns.
     VM& vm = state->vm();
-    IntlNumberFormat* numberFormat = IntlNumberFormat::create(vm, jsCast<IntlNumberFormatConstructor*>(state->callee()));
+    IntlNumberFormatConstructor* callee = jsCast<IntlNumberFormatConstructor*>(state->callee());
+
+    JSValue thisValue = state->thisValue();
+    IntlNumberFormat* numberFormat = jsDynamicCast<IntlNumberFormat*>(thisValue);
+    if (!numberFormat) {
+        JSValue prototype = callee->getDirect(vm, vm.propertyNames->prototype);
+        if (JSObject::defaultHasInstance(state, thisValue, prototype)) {
+            JSObject* thisObject = thisValue.toObject(state);
+            if (state->hadException())
+                return JSValue::encode(jsUndefined());
+
+            numberFormat = IntlNumberFormat::create(vm, callee);
+            numberFormat->initializeNumberFormat(*state, state->argument(0), state->argument(1));
+            if (state->hadException())
+                return JSValue::encode(jsUndefined());
+
+            thisObject->putDirect(vm, vm.propertyNames->intlSubstituteValuePrivateName, numberFormat);
+            return JSValue::encode(thisValue);
+        }
+    }
 
+    // 2. Let numberFormat be OrdinaryCreateFromConstructor(newTarget, %NumberFormatPrototype%).
     // 3. ReturnIfAbrupt(numberFormat).
-    ASSERT(numberFormat);
+    numberFormat = IntlNumberFormat::create(vm, callee);
 
     // 4. Return InitializeNumberFormat(numberFormat, locales, options).
-    JSValue locales = state->argument(0);
-    JSValue options = state->argument(1);
-    numberFormat->initializeNumberFormat(*state, locales, options);
+    numberFormat->initializeNumberFormat(*state, state->argument(0), state->argument(1));
     return JSValue::encode(numberFormat);
 }
 
index 4bb2ee4..7cdbeb4 100644 (file)
@@ -106,6 +106,11 @@ EncodedJSValue JSC_HOST_CALL IntlNumberFormatPrototypeGetterFormat(ExecState* st
     // 11.3.3 Intl.NumberFormat.prototype.format (ECMA-402 2.0)
     // 1. Let nf be this NumberFormat object.
     IntlNumberFormat* nf = jsDynamicCast<IntlNumberFormat*>(state->thisValue());
+
+    // FIXME: Workaround to provide compatibility with ECMA-402 1.0 call/apply patterns.
+    if (!nf)
+        nf = jsDynamicCast<IntlNumberFormat*>(state->thisValue().get(state, state->vm().propertyNames->intlSubstituteValuePrivateName));
+
     if (!nf)
         return JSValue::encode(throwTypeError(state, ASCIILiteral("Intl.NumberFormat.prototype.format called on value that's not an object initialized as a NumberFormat")));
     
@@ -136,6 +141,11 @@ EncodedJSValue JSC_HOST_CALL IntlNumberFormatPrototypeFuncResolvedOptions(ExecSt
 {
     // 11.3.5 Intl.NumberFormat.prototype.resolvedOptions() (ECMA-402 2.0)
     IntlNumberFormat* numberFormat = jsDynamicCast<IntlNumberFormat*>(state->thisValue());
+
+    // FIXME: Workaround to provide compatibility with ECMA-402 1.0 call/apply patterns.
+    if (!numberFormat)
+        numberFormat = jsDynamicCast<IntlNumberFormat*>(state->thisValue().get(state, state->vm().propertyNames->intlSubstituteValuePrivateName));
+
     if (!numberFormat)
         return JSValue::encode(throwTypeError(state, ASCIILiteral("Intl.NumberFormat.prototype.resolvedOptions called on value that's not an object initialized as a NumberFormat")));