Title: [289166] trunk
Revision
289166
Author
commit-qu...@webkit.org
Date
2022-02-06 04:34:45 -0800 (Sun, 06 Feb 2022)

Log Message

Object literal doesn't properly resolve name clash between an accessor and a constant property
https://bugs.webkit.org/show_bug.cgi?id=220574

Patch by Alexey Shvayka <ashva...@apple.com> on 2022-02-06
Reviewed by Yusuke Suzuki.

JSTests:

* stress/class-static-accessor-name-clash-with-field.js: Added.
* stress/object-literal-accessor-name-clash-with-constant.js: Added.

Source/_javascript_Core:

The spec [1] calls [[DefineOwnProperty]] for every property node, whether it's a
getter, a setter, or a value. JSC attempts to reduce emitted bytecodes by setting
up a getter and a setter at once.

However, there is a slower path that exactly matches the spec, which was called only
if a spread syntax or a computed property was encountered. With this patch, the slower
path is also taken in case of a constant property (including a shorthand) with the
same name as an accessor.

That causes an incomplete accessor descriptor to correctly overwrite the existing
data one, which aligns JSC with V8 and SpiderMonkey.

This bug doesn't exist for static class fields and accessors because initialization
of class fields is deferred [2] and they always overwrite eponymous static methods /
accessors, no matter the order in source code. No reproduction for private elements either.

[1]: https://tc39.es/ecma262/#sec-runtime-semantics-methoddefinitionevaluation (step 11 of "get", step 10 of "set")
[2]: https://tc39.es/ecma262/#sec-runtime-semantics-classdefinitionevaluation (step 31.a)

* bytecompiler/NodesCodegen.cpp:
(JSC::PropertyListNode::emitBytecode):

LayoutTests:

Adjusted test now passes on V8 and SpiderMonkey as well.

* js/class-syntax-method-names-expected.txt:
* js/script-tests/class-syntax-method-names.js:

Modified Paths

Added Paths

Diff

Modified: trunk/JSTests/ChangeLog (289165 => 289166)


--- trunk/JSTests/ChangeLog	2022-02-06 00:54:14 UTC (rev 289165)
+++ trunk/JSTests/ChangeLog	2022-02-06 12:34:45 UTC (rev 289166)
@@ -1,3 +1,13 @@
+2022-02-06  Alexey Shvayka  <ashva...@apple.com>
+
+        Object literal doesn't properly resolve name clash between an accessor and a constant property
+        https://bugs.webkit.org/show_bug.cgi?id=220574
+
+        Reviewed by Yusuke Suzuki.
+
+        * stress/class-static-accessor-name-clash-with-field.js: Added.
+        * stress/object-literal-accessor-name-clash-with-constant.js: Added.
+
 2022-02-05  Alexey Shvayka  <ashva...@apple.com>
 
         Attempting to [[Set]] JSArray's read-only "length" should throw even with current [[Value]]

Added: trunk/JSTests/stress/class-static-accessor-name-clash-with-field.js (0 => 289166)


--- trunk/JSTests/stress/class-static-accessor-name-clash-with-field.js	                        (rev 0)
+++ trunk/JSTests/stress/class-static-accessor-name-clash-with-field.js	2022-02-06 12:34:45 UTC (rev 289166)
@@ -0,0 +1,76 @@
+function shouldBe(actual, expected) {
+    if (actual !== expected)
+        throw new Error(`Bad value: ${actual}!`);
+}
+
+let objFooSetterCalls = 0;
+let obj;
+
+obj = class {
+    static get foo() { return "fooGetter"; }
+    static foo = "fooValue"
+    static set foo(_val) { objFooSetterCalls++; }
+};
+
+shouldBe(obj.foo, "fooValue");
+obj.foo = 1;
+shouldBe(obj.foo, 1);
+shouldBe(objFooSetterCalls, 0);
+
+
+obj = class {
+    static set foo(_val) { objFooSetterCalls++; }
+    static foo = "fooValue"
+    static get foo() { return "fooGetter"; }
+};
+
+shouldBe(obj.foo, "fooValue");
+obj.foo = 1;
+shouldBe(obj.foo, 1);
+shouldBe(objFooSetterCalls, 0);
+
+
+obj = class {
+    static foo = "fooValue";
+    static get foo() { return "fooGetter"; }
+};
+
+shouldBe(obj.foo, "fooValue");
+obj.foo = 1;
+shouldBe(obj.foo, 1);
+shouldBe(objFooSetterCalls, 0);
+
+
+obj = class {
+    static foo = "fooValue"
+    static set foo(_val) { objFooSetterCalls++; }
+};
+
+shouldBe(obj.foo, "fooValue");
+obj.foo = 1;
+shouldBe(obj.foo, 1);
+shouldBe(objFooSetterCalls, 0);
+
+
+obj = class {
+    static get foo() { return "fooGetter"; }
+    static set foo(_val) { objFooSetterCalls++; }
+    static foo = "fooValue"
+};
+
+shouldBe(obj.foo, "fooValue");
+obj.foo = 1;
+shouldBe(obj.foo, 1);
+shouldBe(objFooSetterCalls, 0);
+
+
+obj = class {
+    static foo = "fooValue"
+    static get foo() { return "fooGetter"; }
+    static set foo(_val) { objFooSetterCalls++; }
+};
+
+shouldBe(obj.foo, "fooValue");
+obj.foo = 1;
+shouldBe(obj.foo, 1);
+shouldBe(objFooSetterCalls, 0);

Added: trunk/JSTests/stress/object-literal-accessor-name-clash-with-constant.js (0 => 289166)


--- trunk/JSTests/stress/object-literal-accessor-name-clash-with-constant.js	                        (rev 0)
+++ trunk/JSTests/stress/object-literal-accessor-name-clash-with-constant.js	2022-02-06 12:34:45 UTC (rev 289166)
@@ -0,0 +1,93 @@
+function shouldBe(actual, expected) {
+    if (actual !== expected)
+        throw new Error(`Bad value: ${actual}!`);
+}
+
+function shouldThrow(func, expectedError) {
+    let errorThrown = false;
+    try {
+        func();
+    } catch (error) {
+        errorThrown = true;
+        if (error.toString() !== expectedError)
+            throw new Error(`Bad error: ${error}!`);
+    }
+    if (!errorThrown)
+        throw new Error(`Didn't throw!`);
+};
+
+let objFooSetterCalls = 0;
+let obj;
+
+const foo = "fooValue";
+
+obj = {
+    get foo() { return "fooGetter"; },
+    foo: "fooValue",
+    set foo(_val) { objFooSetterCalls++; },
+};
+
+shouldBe(obj.foo, undefined);
+obj.foo = 1;
+shouldBe(obj.foo, undefined);
+shouldBe(objFooSetterCalls, 1);
+
+
+obj = {
+    set foo(_val) { objFooSetterCalls++; },
+    foo,
+    get foo() { return "fooGetter"; },
+};
+
+shouldBe(obj.foo, "fooGetter");
+obj.foo = 1;
+shouldThrow(() => { "use strict"; obj.foo = 2; }, "TypeError: Attempted to assign to readonly property.");
+shouldBe(obj.foo, "fooGetter");
+shouldBe(objFooSetterCalls, 1);
+
+
+obj = {
+    foo: "fooValue",
+    get foo() { return "fooGetter"; },
+};
+
+shouldBe(obj.foo, "fooGetter");
+obj.foo = 1;
+shouldThrow(() => { "use strict"; obj.foo = 2; }, "TypeError: Attempted to assign to readonly property.");
+shouldBe(obj.foo, "fooGetter");
+shouldBe(objFooSetterCalls, 1);
+
+
+obj = {
+    foo,
+    set foo(_val) { objFooSetterCalls++; },
+};
+
+shouldBe(obj.foo, undefined);
+obj.foo = 1;
+shouldBe(obj.foo, undefined);
+shouldBe(objFooSetterCalls, 2);
+
+
+obj = {
+    foo: "fooValue",
+    get foo() { return "fooGetter"; },
+    set foo(_val) { objFooSetterCalls++; },
+};
+
+shouldBe(obj.foo, "fooGetter");
+obj.foo = 1;
+shouldBe(obj.foo, "fooGetter");
+shouldBe(objFooSetterCalls, 3);
+
+
+obj = {
+    get foo() { return "fooGetter"; },
+    set foo(_val) { objFooSetterCalls++; },
+    foo,
+};
+
+shouldBe(obj.foo, "fooValue");
+obj.foo = 1;
+shouldBe(obj.foo, 1);
+shouldBe(objFooSetterCalls, 3);

Modified: trunk/LayoutTests/ChangeLog (289165 => 289166)


--- trunk/LayoutTests/ChangeLog	2022-02-06 00:54:14 UTC (rev 289165)
+++ trunk/LayoutTests/ChangeLog	2022-02-06 12:34:45 UTC (rev 289166)
@@ -1,3 +1,15 @@
+2022-02-06  Alexey Shvayka  <ashva...@apple.com>
+
+        Object literal doesn't properly resolve name clash between an accessor and a constant property
+        https://bugs.webkit.org/show_bug.cgi?id=220574
+
+        Reviewed by Yusuke Suzuki.
+
+        Adjusted test now passes on V8 and SpiderMonkey as well.
+
+        * js/class-syntax-method-names-expected.txt:
+        * js/script-tests/class-syntax-method-names.js:
+
 2022-02-05  Chris Dumez  <cdu...@apple.com>
 
         Resync web-platform-tests/dom from upstream

Modified: trunk/LayoutTests/js/class-syntax-method-names-expected.txt (289165 => 289166)


--- trunk/LayoutTests/js/class-syntax-method-names-expected.txt	2022-02-06 00:54:14 UTC (rev 289165)
+++ trunk/LayoutTests/js/class-syntax-method-names-expected.txt	2022-02-06 12:34:45 UTC (rev 289166)
@@ -87,8 +87,8 @@
 PASS setterValue = undefined; class A { a() { return 423 } set a(x) { setterValue = x } }; (new A).a = 424; setterValue is 424
 PASS setterValue = undefined; class A { set a(x) { setterValue = x } a() { return 425 } }; (new A).a() is 425
 PASS setterValue = undefined; class A { get foo() { return 426 } set foo(x) { setterValue = x; } }; a = new A; a.foo = a.foo; setterValue is 426
-PASS class A { get foo() { } foo() { } set foo(x) { } }; valueTypes((new A).__proto__, 'foo') is ['value']
-PASS class A { set foo(x) { } foo() { } get foo() { } }; valueTypes((new A).__proto__, 'foo') is ['value']
+PASS class A { get foo() { } foo() { } set foo(x) { } }; valueTypes((new A).__proto__, 'foo') is ['get', 'set']
+PASS class A { set foo(x) { } foo() { } get foo() { } }; valueTypes((new A).__proto__, 'foo') is ['get', 'set']
 PASS class A { foo() { } get foo() { } set foo(x) { } }; valueTypes((new A).__proto__, 'foo') is ['get', 'set']
 PASS class A { foo() { } set foo(x) { } get foo() { } }; valueTypes((new A).__proto__, 'foo') is ['get', 'set']
 PASS class A { get foo() { } set foo(x) { } foo() { } }; valueTypes((new A).__proto__, 'foo') is ['value']
@@ -108,8 +108,8 @@
 PASS setterValue = undefined; class A { static a() { return 523 } static set a(x) { setterValue = x } }; A.a = 524; setterValue is 524
 PASS setterValue = undefined; class A { static set a(x) { setterValue = x } static a() { return 525 } }; A.a() is 525
 PASS setterValue = undefined; class A { static get foo() { return 526 } static set foo(x) { setterValue = x; } }; A.foo = A.foo; setterValue is 526
-PASS class A { static get foo() { } static foo() { } static set foo(x) { } }; valueTypes(A, 'foo') is ['value']
-PASS class A { static set foo(x) { } static foo() { } static get foo() { } }; valueTypes(A, 'foo') is ['value']
+PASS class A { static get foo() { } static foo() { } static set foo(x) { } }; valueTypes(A, 'foo') is ['get', 'set']
+PASS class A { static set foo(x) { } static foo() { } static get foo() { } }; valueTypes(A, 'foo') is ['get', 'set']
 PASS class A { static foo() { } static get foo() { } static set foo(x) { } }; valueTypes(A, 'foo') is ['get', 'set']
 PASS class A { static foo() { } static set foo(x) { } static get foo() { } }; valueTypes(A, 'foo') is ['get', 'set']
 PASS class A { static get foo() { } static set foo(x) { } static foo() { } }; valueTypes(A, 'foo') is ['value']

Modified: trunk/LayoutTests/js/script-tests/class-syntax-method-names.js (289165 => 289166)


--- trunk/LayoutTests/js/script-tests/class-syntax-method-names.js	2022-02-06 00:54:14 UTC (rev 289165)
+++ trunk/LayoutTests/js/script-tests/class-syntax-method-names.js	2022-02-06 12:34:45 UTC (rev 289166)
@@ -95,8 +95,8 @@
     descriptor = Object.getOwnPropertyDescriptor(object, proeprtyName);
     return ['value', 'get', 'set'].filter(function (name) { return name in descriptor; });
 }
-shouldBe("class A { get foo() { } foo() { } set foo(x) { } }; valueTypes((new A).__proto__, 'foo')", "['value']");
-shouldBe("class A { set foo(x) { } foo() { } get foo() { } }; valueTypes((new A).__proto__, 'foo')", "['value']");
+shouldBe("class A { get foo() { } foo() { } set foo(x) { } }; valueTypes((new A).__proto__, 'foo')", "['get', 'set']");
+shouldBe("class A { set foo(x) { } foo() { } get foo() { } }; valueTypes((new A).__proto__, 'foo')", "['get', 'set']");
 shouldBe("class A { foo() { } get foo() { } set foo(x) { } }; valueTypes((new A).__proto__, 'foo')", "['get', 'set']");
 shouldBe("class A { foo() { } set foo(x) { } get foo() { } }; valueTypes((new A).__proto__, 'foo')", "['get', 'set']");
 shouldBe("class A { get foo() { } set foo(x) { } foo() { } }; valueTypes((new A).__proto__, 'foo')", "['value']");
@@ -117,8 +117,8 @@
 shouldBe("setterValue = undefined; class A { static a() { return 523 } static set a(x) { setterValue = x } }; A.a = 524; setterValue", "524");
 shouldBe("setterValue = undefined; class A { static set a(x) { setterValue = x } static a() { return 525 } }; A.a()", "525");
 shouldBe("setterValue = undefined; class A { static get foo() { return 526 } static set foo(x) { setterValue = x; } }; A.foo = A.foo; setterValue", "526");
-shouldBe("class A { static get foo() { } static foo() { } static set foo(x) { } }; valueTypes(A, 'foo')", "['value']");
-shouldBe("class A { static set foo(x) { } static foo() { } static get foo() { } }; valueTypes(A, 'foo')", "['value']");
+shouldBe("class A { static get foo() { } static foo() { } static set foo(x) { } }; valueTypes(A, 'foo')", "['get', 'set']");
+shouldBe("class A { static set foo(x) { } static foo() { } static get foo() { } }; valueTypes(A, 'foo')", "['get', 'set']");
 shouldBe("class A { static foo() { } static get foo() { } static set foo(x) { } }; valueTypes(A, 'foo')", "['get', 'set']");
 shouldBe("class A { static foo() { } static set foo(x) { } static get foo() { } }; valueTypes(A, 'foo')", "['get', 'set']");
 shouldBe("class A { static get foo() { } static set foo(x) { } static foo() { } }; valueTypes(A, 'foo')", "['value']");

Modified: trunk/Source/_javascript_Core/ChangeLog (289165 => 289166)


--- trunk/Source/_javascript_Core/ChangeLog	2022-02-06 00:54:14 UTC (rev 289165)
+++ trunk/Source/_javascript_Core/ChangeLog	2022-02-06 12:34:45 UTC (rev 289166)
@@ -1,3 +1,32 @@
+2022-02-06  Alexey Shvayka  <ashva...@apple.com>
+
+        Object literal doesn't properly resolve name clash between an accessor and a constant property
+        https://bugs.webkit.org/show_bug.cgi?id=220574
+
+        Reviewed by Yusuke Suzuki.
+
+        The spec [1] calls [[DefineOwnProperty]] for every property node, whether it's a
+        getter, a setter, or a value. JSC attempts to reduce emitted bytecodes by setting
+        up a getter and a setter at once.
+
+        However, there is a slower path that exactly matches the spec, which was called only
+        if a spread syntax or a computed property was encountered. With this patch, the slower
+        path is also taken in case of a constant property (including a shorthand) with the
+        same name as an accessor.
+
+        That causes an incomplete accessor descriptor to correctly overwrite the existing
+        data one, which aligns JSC with V8 and SpiderMonkey.
+
+        This bug doesn't exist for static class fields and accessors because initialization
+        of class fields is deferred [2] and they always overwrite eponymous static methods /
+        accessors, no matter the order in source code. No reproduction for private elements either.
+
+        [1]: https://tc39.es/ecma262/#sec-runtime-semantics-methoddefinitionevaluation (step 11 of "get", step 10 of "set")
+        [2]: https://tc39.es/ecma262/#sec-runtime-semantics-classdefinitionevaluation (step 31.a)
+
+        * bytecompiler/NodesCodegen.cpp:
+        (JSC::PropertyListNode::emitBytecode):
+
 2022-02-05  Alexey Shvayka  <ashva...@apple.com>
 
         Attempting to [[Set]] JSArray's read-only "length" should throw even with current [[Value]]

Modified: trunk/Source/_javascript_Core/bytecompiler/NodesCodegen.cpp (289165 => 289166)


--- trunk/Source/_javascript_Core/bytecompiler/NodesCodegen.cpp	2022-02-06 00:54:14 UTC (rev 289165)
+++ trunk/Source/_javascript_Core/bytecompiler/NodesCodegen.cpp	2022-02-06 12:34:45 UTC (rev 289166)
@@ -661,7 +661,8 @@
     // Were there any get/set properties?
     if (p) {
         // Build a list of getter/setter pairs to try to put them at the same time. If we encounter
-        // a computed property or a spread, just emit everything as that may override previous values.
+        // a constant property by the same name as accessor or a computed property or a spread,
+        // just emit everything as that may override previous values.
         bool canOverrideProperties = false;
 
         GetterSetterMap instanceMap;
@@ -675,12 +676,17 @@
                 break;
             }
 
-            if (node->m_type & PropertyNode::Constant)
+            GetterSetterMap& map = node->isStaticClassProperty() ? staticMap : instanceMap;
+            if (node->m_type & PropertyNode::Constant) {
+                if (map.contains(node->name()->impl())) {
+                    canOverrideProperties = true;
+                    break;
+                }
                 continue;
+            }
 
             // Duplicates are possible.
             GetterSetterPair pair(node, static_cast<PropertyNode*>(nullptr));
-            GetterSetterMap& map = node->isStaticClassProperty() ? staticMap : instanceMap;
             GetterSetterMap::AddResult result = map.add(node->name()->impl(), pair);
             auto& resultPair = result.iterator->value;
             if (!result.isNewEntry) {
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to