ES6: Classes: Program level class statement throws exception in strict mode
authorjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 25 Mar 2015 21:33:59 +0000 (21:33 +0000)
committerjoepeck@webkit.org <joepeck@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 25 Mar 2015 21:33:59 +0000 (21:33 +0000)
https://bugs.webkit.org/show_bug.cgi?id=143038

Reviewed by Ryosuke Niwa.

Source/JavaScriptCore:

Classes expose a name to the current lexical environment. This treats
"class X {}" like "var X = class X {}". Ideally it would be "let X = class X {}".
Also, improve error messages for class statements where the class is missing a name.

* parser/Parser.h:
* parser/Parser.cpp:
(JSC::Parser<LexerType>::parseClass):
Fill name in info parameter if needed. Better error message if name is needed and missing.

(JSC::Parser<LexerType>::parseClassDeclaration):
Pass info parameter to get name, and expose the name as a variable name.

(JSC::Parser<LexerType>::parsePrimaryExpression):
Pass info parameter that is ignored.

* parser/ParserFunctionInfo.h:
Add a parser info for class, to extract the name.

LayoutTests:

This updates a number of existing tests that were relying on
poor behavior. `shouldBe` and friends use eval within a function
not at the global scope. This means `shouldBe('class X { ... }')`
behaves like `shouldBe('var x = ...')` not `shouldBe('x = ...')`.
This means `x` will not be available in the next `shouldBe` call.

Add a test specifically to cover the scoping of the class name
in regular and strict mode code. Currently we treat it like var
with one failing test that would pass when we treat it like let.

* js/class-syntax-name.html: Added.
* js/script-tests/class-syntax-name.js: Added.
(runTestShouldBe):
(runTestShouldBeTrue):
(runTestShouldThrow):
(runTestShouldNotThrow):
Test class name scoping.

* js/class-syntax-call-expected.txt:
* js/class-syntax-declaration-expected.txt:
* js/class-syntax-default-constructor-expected.txt:
* js/class-syntax-name-expected.txt: Added.
* js/script-tests/class-syntax-call.js:
* js/script-tests/class-syntax-declaration.js:
* js/script-tests/class-syntax-default-constructor.js:

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

14 files changed:
LayoutTests/ChangeLog
LayoutTests/js/class-syntax-call-expected.txt
LayoutTests/js/class-syntax-declaration-expected.txt
LayoutTests/js/class-syntax-default-constructor-expected.txt
LayoutTests/js/class-syntax-name-expected.txt [new file with mode: 0644]
LayoutTests/js/class-syntax-name.html [new file with mode: 0644]
LayoutTests/js/script-tests/class-syntax-call.js
LayoutTests/js/script-tests/class-syntax-declaration.js
LayoutTests/js/script-tests/class-syntax-default-constructor.js
LayoutTests/js/script-tests/class-syntax-name.js [new file with mode: 0644]
Source/JavaScriptCore/ChangeLog
Source/JavaScriptCore/parser/Parser.cpp
Source/JavaScriptCore/parser/Parser.h
Source/JavaScriptCore/parser/ParserFunctionInfo.h

index 8677f6cda044a003bce8bc6168ec94d89df4ae8f..ae48753838d599f57bea3288f418a0c5f00bf918 100644 (file)
@@ -1,3 +1,36 @@
+2015-03-25  Joseph Pecoraro  <pecoraro@apple.com>
+
+        ES6: Classes: Program level class statement throws exception in strict mode
+        https://bugs.webkit.org/show_bug.cgi?id=143038
+
+        Reviewed by Ryosuke Niwa.
+
+        This updates a number of existing tests that were relying on
+        poor behavior. `shouldBe` and friends use eval within a function
+        not at the global scope. This means `shouldBe('class X { ... }')`
+        behaves like `shouldBe('var x = ...')` not `shouldBe('x = ...')`.
+        This means `x` will not be available in the next `shouldBe` call.
+
+        Add a test specifically to cover the scoping of the class name
+        in regular and strict mode code. Currently we treat it like var
+        with one failing test that would pass when we treat it like let.
+
+        * js/class-syntax-name.html: Added.
+        * js/script-tests/class-syntax-name.js: Added.
+        (runTestShouldBe):
+        (runTestShouldBeTrue):
+        (runTestShouldThrow):
+        (runTestShouldNotThrow):
+        Test class name scoping.
+
+        * js/class-syntax-call-expected.txt:
+        * js/class-syntax-declaration-expected.txt:
+        * js/class-syntax-default-constructor-expected.txt:
+        * js/class-syntax-name-expected.txt: Added.
+        * js/script-tests/class-syntax-call.js:
+        * js/script-tests/class-syntax-declaration.js:
+        * js/script-tests/class-syntax-default-constructor.js:
+
 2015-03-25  Mark Lam  <mark.lam@apple.com>
 
         Gardening: rebaseline after r181907.
index bfc0318abaea76f6504641b5dfa3dde9336a3845..3349ae8cfc001af900b5f76bd69965d5657dae89 100644 (file)
@@ -3,9 +3,9 @@ Tests for calling the constructors of ES6 classes
 On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
 
 
-PASS class A { constructor() {} }; new A did not throw exception.
+PASS new A did not throw exception.
 PASS A() threw exception TypeError: Cannot call a class constructor.
-PASS class B extends A { constructor() { super() } }; new B did not throw exception.
+PASS new B did not throw exception.
 PASS B() threw exception TypeError: Cannot call a class constructor.
 PASS new (class { constructor() {} })() did not throw exception.
 PASS (class { constructor() {} })() threw exception TypeError: Cannot call a class constructor.
index f9c87b5c658b6f4f49e84db1f5b98e8e82d06fc4..59ed67f1a66d8a179f41f62559db66b91a32f3d9 100644 (file)
@@ -16,15 +16,17 @@ PASS setterValue is 789
 PASS (new A).__proto__ is A.prototype
 PASS A.prototype.constructor is A
 PASS class threw exception SyntaxError: Unexpected end of script.
+PASS class [ threw exception SyntaxError: Unexpected token '['.
+PASS class { threw exception SyntaxError: Class statements must have a name..
 PASS class X { threw exception SyntaxError: Unexpected end of script.
 PASS class X { ( } threw exception SyntaxError: Unexpected token '('. Expected an identifier..
 PASS class X {} did not throw exception.
 PASS class X { constructor() {} constructor() {} } threw exception SyntaxError: Cannot declare multiple constructors in a single class..
 PASS class X { constructor() {} static constructor() { return staticMethodValue; } } did not throw exception.
-PASS X.constructor() is staticMethodValue
+PASS class X { constructor() {} static constructor() { return staticMethodValue; } }; X.constructor() is staticMethodValue
 PASS class X { constructor() {} static prototype() {} } threw exception SyntaxError: Cannot declare a static method named 'prototype'..
 PASS class X { constructor() {} prototype() { return instanceMethodValue; } } did not throw exception.
-PASS (new X).prototype() is instanceMethodValue
+PASS class X { constructor() {} prototype() { return instanceMethodValue; } }; (new X).prototype() is instanceMethodValue
 PASS class X { constructor() {} set foo(a) {} } did not throw exception.
 PASS class X { constructor() {} set foo({x, y}) {} } did not throw exception.
 PASS class X { constructor() {} set foo() {} } threw exception SyntaxError: Unexpected token ')'. setter functions must have one parameter..
index 8553d86794367b9bd9e78397e66a45e23b5e77e3..dba988e97364ffcfd4a2a1da74d9baec7b3571ac 100644 (file)
@@ -3,11 +3,11 @@ Tests for ES6 class syntax default constructor
 On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
 
 
-PASS class A { }; new A instanceof A is true
+PASS new A instanceof A is true
 PASS A() threw exception TypeError: Cannot call a class constructor.
 PASS A.prototype.constructor instanceof Function is true
 PASS A.prototype.constructor.name is "A"
-PASS class B extends A { }; new B instanceof A; new B instanceof A is true
+PASS new B instanceof A; new B instanceof A is true
 PASS B() threw exception TypeError: Cannot call a class constructor.
 PASS B.prototype.constructor.name is "B"
 PASS A !== B is true
diff --git a/LayoutTests/js/class-syntax-name-expected.txt b/LayoutTests/js/class-syntax-name-expected.txt
new file mode 100644 (file)
index 0000000..7f1903a
--- /dev/null
@@ -0,0 +1,122 @@
+Tests for ES6 class name semantics in class statements and expressions
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+Class statement
+PASS A threw exception ReferenceError: Can't find variable: A.
+PASS 'use strict'; A threw exception ReferenceError: Can't find variable: A.
+PASS class {} threw exception SyntaxError: Class statements must have a name..
+PASS 'use strict'; class {} threw exception SyntaxError: Class statements must have a name..
+PASS class { constructor() {} } threw exception SyntaxError: Class statements must have a name..
+PASS 'use strict'; class { constructor() {} } threw exception SyntaxError: Class statements must have a name..
+PASS class A { constructor() {} } did not throw exception.
+PASS 'use strict'; class A { constructor() {} } did not throw exception.
+PASS class A { constructor() {} }; A.toString() is 'function A() {}'
+PASS 'use strict'; class A { constructor() {} }; A.toString() is 'function A() {}'
+PASS class A { constructor() {} }; (new A) instanceof A is true
+PASS 'use strict'; class A { constructor() {} }; (new A) instanceof A is true
+PASS class A { constructor() { this.base = A; } }; (new A).base.toString() is 'function A() { this.base = A; }'
+PASS 'use strict'; class A { constructor() { this.base = A; } }; (new A).base.toString() is 'function A() { this.base = A; }'
+PASS class A { constructor() {} }; class B extends A {}; did not throw exception.
+PASS 'use strict'; class A { constructor() {} }; class B extends A {}; did not throw exception.
+PASS class A { constructor() {} }; class B extends A { constructor() {} }; B.toString() is 'function B() {}'
+PASS 'use strict'; class A { constructor() {} }; class B extends A { constructor() {} }; B.toString() is 'function B() {}'
+PASS class A { constructor() {} }; class B extends A {}; (new B) instanceof A is true
+PASS 'use strict'; class A { constructor() {} }; class B extends A {}; (new B) instanceof A is true
+PASS class A { constructor() {} }; class B extends A {}; (new B) instanceof B is true
+PASS 'use strict'; class A { constructor() {} }; class B extends A {}; (new B) instanceof B is true
+PASS class A { constructor() {} }; class B extends A { constructor() { super(); this.base = A; this.derived = B; } }; (new B).base.toString() is 'function A() {}'
+PASS 'use strict'; class A { constructor() {} }; class B extends A { constructor() { super(); this.base = A; this.derived = B; } }; (new B).base.toString() is 'function A() {}'
+PASS class A { constructor() {} }; class B extends A { constructor() { super(); this.base = A; this.derived = B; } }; (new B).derived.toString() is 'function B() { super(); this.base = A; this.derived = B; }'
+PASS 'use strict'; class A { constructor() {} }; class B extends A { constructor() { super(); this.base = A; this.derived = B; } }; (new B).derived.toString() is 'function B() { super(); this.base = A; this.derived = B; }'
+
+Class expression
+PASS A threw exception ReferenceError: Can't find variable: A.
+PASS 'use strict'; A threw exception ReferenceError: Can't find variable: A.
+PASS (class {}) did not throw exception.
+PASS 'use strict'; (class {}) did not throw exception.
+PASS (class { constructor(){} }) did not throw exception.
+PASS 'use strict'; (class { constructor(){} }) did not throw exception.
+PASS typeof (class {}) is "function"
+PASS 'use strict'; typeof (class {}) is "function"
+PASS (class A {}) did not throw exception.
+PASS 'use strict'; (class A {}) did not throw exception.
+PASS typeof (class A {}) is "function"
+PASS 'use strict'; typeof (class A {}) is "function"
+PASS (class A {}); A threw exception ReferenceError: Can't find variable: A.
+PASS 'use strict'; (class A {}); A threw exception ReferenceError: Can't find variable: A.
+PASS new (class A {}) did not throw exception.
+PASS 'use strict'; new (class A {}) did not throw exception.
+PASS typeof (new (class A {})) is "object"
+PASS 'use strict'; typeof (new (class A {})) is "object"
+PASS (new (class A { constructor() { this.base = A; } })).base did not throw exception.
+PASS 'use strict'; (new (class A { constructor() { this.base = A; } })).base did not throw exception.
+PASS (new (class A { constructor() { this.base = A; } })).base.toString() is "function A() { this.base = A; }"
+PASS 'use strict'; (new (class A { constructor() { this.base = A; } })).base.toString() is "function A() { this.base = A; }"
+PASS class A {}; (class B extends A {}) did not throw exception.
+PASS 'use strict'; class A {}; (class B extends A {}) did not throw exception.
+PASS class A {}; (class B extends A {}); B threw exception ReferenceError: Can't find variable: B.
+PASS 'use strict'; class A {}; (class B extends A {}); B threw exception ReferenceError: Can't find variable: B.
+PASS class A {}; new (class B extends A {}) did not throw exception.
+PASS 'use strict'; class A {}; new (class B extends A {}) did not throw exception.
+PASS class A {}; new (class B extends A { constructor() { super(); this.base = A; this.derived = B; } }) did not throw exception.
+PASS 'use strict'; class A {}; new (class B extends A { constructor() { super(); this.base = A; this.derived = B; } }) did not throw exception.
+PASS class A {}; (new (class B extends A { constructor() { super(); this.base = A; this.derived = B; } })) instanceof A is true
+PASS 'use strict'; class A {}; (new (class B extends A { constructor() { super(); this.base = A; this.derived = B; } })) instanceof A is true
+PASS class A { constructor() {} }; (new (class B extends A { constructor() { super(); this.base = A; this.derived = B; } })).base.toString() is 'function A() {}'
+PASS 'use strict'; class A { constructor() {} }; (new (class B extends A { constructor() { super(); this.base = A; this.derived = B; } })).base.toString() is 'function A() {}'
+PASS class A { constructor() {} }; (new (class B extends A { constructor() { super(); this.base = A; this.derived = B; } })).derived.toString() is 'function B() { super(); this.base = A; this.derived = B; }'
+PASS 'use strict'; class A { constructor() {} }; (new (class B extends A { constructor() { super(); this.base = A; this.derived = B; } })).derived.toString() is 'function B() { super(); this.base = A; this.derived = B; }'
+
+Class expression assignment to variable
+PASS A threw exception ReferenceError: Can't find variable: A.
+PASS 'use strict'; A threw exception ReferenceError: Can't find variable: A.
+PASS var VarA = class {} did not throw exception.
+PASS 'use strict'; var VarA = class {} did not throw exception.
+PASS var VarA = class { constructor() {} }; VarA.toString() is 'function () {}'
+PASS 'use strict'; var VarA = class { constructor() {} }; VarA.toString() is 'function () {}'
+PASS VarA threw exception ReferenceError: Can't find variable: VarA.
+PASS 'use strict'; VarA threw exception ReferenceError: Can't find variable: VarA.
+PASS var VarA = class A { constructor() {} } did not throw exception.
+PASS 'use strict'; var VarA = class A { constructor() {} } did not throw exception.
+PASS var VarA = class A { constructor() {} }; VarA.toString() is 'function A() {}'
+PASS 'use strict'; var VarA = class A { constructor() {} }; VarA.toString() is 'function A() {}'
+PASS var VarA = class A { constructor() {} }; A.toString() threw exception ReferenceError: Can't find variable: A.
+PASS 'use strict'; var VarA = class A { constructor() {} }; A.toString() threw exception ReferenceError: Can't find variable: A.
+PASS var VarA = class A { constructor() {} }; (new VarA) instanceof VarA is true
+PASS 'use strict'; var VarA = class A { constructor() {} }; (new VarA) instanceof VarA is true
+PASS var VarA = class A { constructor() { this.base = A; } }; (new VarA).base.toString() is 'function A() { this.base = A; }'
+PASS 'use strict'; var VarA = class A { constructor() { this.base = A; } }; (new VarA).base.toString() is 'function A() { this.base = A; }'
+PASS var VarA = class A { constructor() {} }; var VarB = class B extends VarA { constructor() {} }; did not throw exception.
+PASS 'use strict'; var VarA = class A { constructor() {} }; var VarB = class B extends VarA { constructor() {} }; did not throw exception.
+PASS var VarA = class A { constructor() {} }; var VarB = class B extends VarA { constructor() {} }; B threw exception ReferenceError: Can't find variable: B.
+PASS 'use strict'; var VarA = class A { constructor() {} }; var VarB = class B extends VarA { constructor() {} }; B threw exception ReferenceError: Can't find variable: B.
+PASS var VarA = class A { constructor() {} }; var VarB = class B extends VarA { constructor() {} }; VarB.toString() is 'function B() {}'
+PASS 'use strict'; var VarA = class A { constructor() {} }; var VarB = class B extends VarA { constructor() {} }; VarB.toString() is 'function B() {}'
+PASS var VarA = class A { constructor() {} }; var VarB = class B extends VarA { }; (new VarB) instanceof VarA is true
+PASS 'use strict'; var VarA = class A { constructor() {} }; var VarB = class B extends VarA { }; (new VarB) instanceof VarA is true
+PASS var VarA = class A { constructor() {} }; var VarB = class B extends VarA { }; (new VarB) instanceof VarB is true
+PASS 'use strict'; var VarA = class A { constructor() {} }; var VarB = class B extends VarA { }; (new VarB) instanceof VarB is true
+PASS var VarA = class A { constructor() {} }; var VarB = class B extends VarA { constructor() { super(); this.base = VarA; this.derived = B; this.derivedVar = VarB; } }; (new VarB).base === VarA is true
+PASS 'use strict'; var VarA = class A { constructor() {} }; var VarB = class B extends VarA { constructor() { super(); this.base = VarA; this.derived = B; this.derivedVar = VarB; } }; (new VarB).base === VarA is true
+PASS var VarA = class A { constructor() {} }; var VarB = class B extends VarA { constructor() { super(); this.base = VarA; this.derived = B; this.derivedVar = VarB; } }; (new VarB).derived === VarB is true
+PASS 'use strict'; var VarA = class A { constructor() {} }; var VarB = class B extends VarA { constructor() { super(); this.base = VarA; this.derived = B; this.derivedVar = VarB; } }; (new VarB).derived === VarB is true
+PASS var VarA = class A { constructor() {} }; var VarB = class B extends VarA { constructor() { super(); this.base = VarA; this.derived = B; this.derivedVar = VarB; } }; (new VarB).derivedVar === VarB is true
+PASS 'use strict'; var VarA = class A { constructor() {} }; var VarB = class B extends VarA { constructor() { super(); this.base = VarA; this.derived = B; this.derivedVar = VarB; } }; (new VarB).derivedVar === VarB is true
+
+Class statement binding in other circumstances
+PASS var result = A; result threw exception ReferenceError: Can't find variable: A.
+PASS 'use strict'; var result = A; result threw exception ReferenceError: Can't find variable: A.
+FAIL var result = A; class A {}; result should throw an exception. Was undefined.
+FAIL 'use strict'; var result = A; class A {}; result should throw an exception. Was undefined.
+PASS class A {}; var result = A; result did not throw exception.
+PASS 'use strict'; class A {}; var result = A; result did not throw exception.
+PASS eval('var Foo = 10'); Foo is 10
+PASS 'use strict'; eval('var Foo = 10'); Foo threw exception ReferenceError: Can't find variable: Foo.
+PASS eval('class Bar { constructor() {} }'); Bar.toString() is 'function Bar() {}'
+PASS 'use strict'; eval('class Bar { constructor() {} }'); Bar.toString() threw exception ReferenceError: Can't find variable: Bar.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/js/class-syntax-name.html b/LayoutTests/js/class-syntax-name.html
new file mode 100644 (file)
index 0000000..6df3c4f
--- /dev/null
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script src="../resources/js-test-pre.js"></script>
+<script src="script-tests/class-syntax-name.js"></script>
+<script src="../resources/js-test-post.js"></script>
+</body>
+</html>
index d324a88d3badd893b14b402bfd1e703e7cde3c6b..62fd37f526fd3227d226b09d700c698b4974d78a 100644 (file)
@@ -1,8 +1,11 @@
 description('Tests for calling the constructors of ES6 classes');
 
-shouldNotThrow('class A { constructor() {} }; new A');
+class A { constructor() {} };
+class B extends A { constructor() { super() } };
+
+shouldNotThrow('new A');
 shouldThrow('A()', '"TypeError: Cannot call a class constructor"');
-shouldNotThrow('class B extends A { constructor() { super() } }; new B');
+shouldNotThrow('new B');
 shouldThrow('B()', '"TypeError: Cannot call a class constructor"');
 shouldNotThrow('new (class { constructor() {} })()');
 shouldThrow('(class { constructor() {} })()', '"TypeError: Cannot call a class constructor"');
index f55d48a62dbd387eb085939230b9c9fded30692e..78a669975ce501cd90275bc42e7917d309b52d17 100644 (file)
@@ -28,15 +28,17 @@ shouldBe("(new A).__proto__", "A.prototype");
 shouldBe("A.prototype.constructor", "A");
 
 shouldThrow("class", "'SyntaxError: Unexpected end of script'");
+shouldThrow("class [", "'SyntaxError: Unexpected token \\'[\\''");
+shouldThrow("class {", "'SyntaxError: Class statements must have a name.'");
 shouldThrow("class X {", "'SyntaxError: Unexpected end of script'");
 shouldThrow("class X { ( }", "'SyntaxError: Unexpected token \\'(\\'. Expected an identifier.'");
 shouldNotThrow("class X {}");
 shouldThrow("class X { constructor() {} constructor() {} }", "'SyntaxError: Cannot declare multiple constructors in a single class.'");
 shouldNotThrow("class X { constructor() {} static constructor() { return staticMethodValue; } }");
-shouldBe("X.constructor()", "staticMethodValue");
+shouldBe("class X { constructor() {} static constructor() { return staticMethodValue; } }; X.constructor()", "staticMethodValue");
 shouldThrow("class X { constructor() {} static prototype() {} }", "'SyntaxError: Cannot declare a static method named \\'prototype\\'.'");
 shouldNotThrow("class X { constructor() {} prototype() { return instanceMethodValue; } }");
-shouldBe("(new X).prototype()", "instanceMethodValue");
+shouldBe("class X { constructor() {} prototype() { return instanceMethodValue; } }; (new X).prototype()", "instanceMethodValue");
 
 shouldNotThrow("class X { constructor() {} set foo(a) {} }");
 shouldNotThrow("class X { constructor() {} set foo({x, y}) {} }");
index 5cb7ca4f2d31ee2a8f6375b478c034e83f42c1a4..c2cbcee2b1a8560ecaf0fe3227b89c0dd2a680d3 100644 (file)
@@ -1,11 +1,14 @@
 
 description('Tests for ES6 class syntax default constructor');
 
-shouldBeTrue('class A { }; new A instanceof A');
+class A { };
+class B extends A { };
+
+shouldBeTrue('new A instanceof A');
 shouldThrow('A()', '"TypeError: Cannot call a class constructor"');
 shouldBeTrue('A.prototype.constructor instanceof Function');
 shouldBe('A.prototype.constructor.name', '"A"');
-shouldBeTrue('class B extends A { }; new B instanceof A; new B instanceof A');
+shouldBeTrue('new B instanceof A; new B instanceof A');
 shouldThrow('B()', '"TypeError: Cannot call a class constructor"');
 shouldBe('B.prototype.constructor.name', '"B"');
 shouldBeTrue('A !== B');
diff --git a/LayoutTests/js/script-tests/class-syntax-name.js b/LayoutTests/js/script-tests/class-syntax-name.js
new file mode 100644 (file)
index 0000000..516e502
--- /dev/null
@@ -0,0 +1,88 @@
+description('Tests for ES6 class name semantics in class statements and expressions');
+
+function runTestShouldBe(statement, result) {
+    shouldBe(statement, result);
+    shouldBe("'use strict'; " + statement, result);
+}
+
+function runTestShouldBeTrue(statement) {
+    shouldBeTrue(statement);
+    shouldBeTrue("'use strict'; " + statement);
+}
+
+function runTestShouldThrow(statement) {
+    shouldThrow(statement);
+    shouldThrow("'use strict'; " + statement);
+}
+
+function runTestShouldNotThrow(statement) {
+    shouldNotThrow(statement);
+    shouldNotThrow("'use strict'; " + statement);
+}
+
+// Class statement. Class name added to global scope. Class name is available inside class scope and in global scope.
+debug('Class statement');
+runTestShouldThrow("A");
+runTestShouldThrow("class {}");
+runTestShouldThrow("class { constructor() {} }");
+runTestShouldNotThrow("class A { constructor() {} }");
+runTestShouldBe("class A { constructor() {} }; A.toString()", "'function A() {}'");
+runTestShouldBeTrue("class A { constructor() {} }; (new A) instanceof A");
+runTestShouldBe("class A { constructor() { this.base = A; } }; (new A).base.toString()", "'function A() { this.base = A; }'");
+runTestShouldNotThrow("class A { constructor() {} }; class B extends A {};");
+runTestShouldBe("class A { constructor() {} }; class B extends A { constructor() {} }; B.toString()", "'function B() {}'");
+runTestShouldBeTrue("class A { constructor() {} }; class B extends A {}; (new B) instanceof A");
+runTestShouldBeTrue("class A { constructor() {} }; class B extends A {}; (new B) instanceof B");
+runTestShouldBe("class A { constructor() {} }; class B extends A { constructor() { super(); this.base = A; this.derived = B; } }; (new B).base.toString()", "'function A() {}'");
+runTestShouldBe("class A { constructor() {} }; class B extends A { constructor() { super(); this.base = A; this.derived = B; } }; (new B).derived.toString()", "'function B() { super(); this.base = A; this.derived = B; }'");
+
+// Class expression. Class name not added to scope. Class name is available inside class scope.
+debug(''); debug('Class expression');
+runTestShouldThrow("A");
+runTestShouldNotThrow("(class {})");
+runTestShouldNotThrow("(class { constructor(){} })");
+runTestShouldBe("typeof (class {})", '"function"');
+runTestShouldNotThrow("(class A {})");
+runTestShouldBe("typeof (class A {})", '"function"');
+runTestShouldThrow("(class A {}); A");
+runTestShouldNotThrow("new (class A {})");
+runTestShouldBe("typeof (new (class A {}))", '"object"');
+runTestShouldNotThrow("(new (class A { constructor() { this.base = A; } })).base");
+runTestShouldBe("(new (class A { constructor() { this.base = A; } })).base.toString()", '"function A() { this.base = A; }"');
+runTestShouldNotThrow("class A {}; (class B extends A {})");
+runTestShouldThrow("class A {}; (class B extends A {}); B");
+runTestShouldNotThrow("class A {}; new (class B extends A {})");
+runTestShouldNotThrow("class A {}; new (class B extends A { constructor() { super(); this.base = A; this.derived = B; } })");
+runTestShouldBeTrue("class A {}; (new (class B extends A { constructor() { super(); this.base = A; this.derived = B; } })) instanceof A");
+runTestShouldBe("class A { constructor() {} }; (new (class B extends A { constructor() { super(); this.base = A; this.derived = B; } })).base.toString()", "'function A() {}'");
+runTestShouldBe("class A { constructor() {} }; (new (class B extends A { constructor() { super(); this.base = A; this.derived = B; } })).derived.toString()", "'function B() { super(); this.base = A; this.derived = B; }'");
+
+// Assignment of a class expression to a variable. Variable name available in scope, class name is not. Class name is available inside class scope.
+debug(''); debug('Class expression assignment to variable');
+runTestShouldThrow("A");
+runTestShouldNotThrow("var VarA = class {}");
+runTestShouldBe("var VarA = class { constructor() {} }; VarA.toString()", "'function () {}'");
+runTestShouldThrow("VarA");
+runTestShouldNotThrow("var VarA = class A { constructor() {} }");
+runTestShouldBe("var VarA = class A { constructor() {} }; VarA.toString()", "'function A() {}'");
+runTestShouldThrow("var VarA = class A { constructor() {} }; A.toString()");
+runTestShouldBeTrue("var VarA = class A { constructor() {} }; (new VarA) instanceof VarA");
+runTestShouldBe("var VarA = class A { constructor() { this.base = A; } }; (new VarA).base.toString()", "'function A() { this.base = A; }'");
+runTestShouldNotThrow("var VarA = class A { constructor() {} }; var VarB = class B extends VarA { constructor() {} };");
+runTestShouldThrow("var VarA = class A { constructor() {} }; var VarB = class B extends VarA { constructor() {} }; B");
+runTestShouldBe("var VarA = class A { constructor() {} }; var VarB = class B extends VarA { constructor() {} }; VarB.toString()", "'function B() {}'");
+runTestShouldBeTrue("var VarA = class A { constructor() {} }; var VarB = class B extends VarA { }; (new VarB) instanceof VarA");
+runTestShouldBeTrue("var VarA = class A { constructor() {} }; var VarB = class B extends VarA { }; (new VarB) instanceof VarB");
+runTestShouldBeTrue("var VarA = class A { constructor() {} }; var VarB = class B extends VarA { constructor() { super(); this.base = VarA; this.derived = B; this.derivedVar = VarB; } }; (new VarB).base === VarA");
+runTestShouldBeTrue("var VarA = class A { constructor() {} }; var VarB = class B extends VarA { constructor() { super(); this.base = VarA; this.derived = B; this.derivedVar = VarB; } }; (new VarB).derived === VarB");
+runTestShouldBeTrue("var VarA = class A { constructor() {} }; var VarB = class B extends VarA { constructor() { super(); this.base = VarA; this.derived = B; this.derivedVar = VarB; } }; (new VarB).derivedVar === VarB");
+
+// FIXME: Class statement binding should be like `let`, not `var`.
+debug(''); debug('Class statement binding in other circumstances');
+runTestShouldThrow("var result = A; result");
+runTestShouldThrow("var result = A; class A {}; result");
+runTestShouldNotThrow("class A {}; var result = A; result");
+shouldBe("eval('var Foo = 10'); Foo", "10");
+shouldThrow("'use strict'; eval('var Foo = 10'); Foo");
+shouldBe("eval('class Bar { constructor() {} }'); Bar.toString()", "'function Bar() {}'");
+shouldThrow("'use strict'; eval('class Bar { constructor() {} }'); Bar.toString()");
index 43532103159436396e093ff7820f0e2763ffdb16..c8592eb548bff7419776b6339f5a782d6031e19b 100644 (file)
@@ -1,3 +1,28 @@
+2015-03-25  Joseph Pecoraro  <pecoraro@apple.com>
+
+        ES6: Classes: Program level class statement throws exception in strict mode
+        https://bugs.webkit.org/show_bug.cgi?id=143038
+
+        Reviewed by Ryosuke Niwa.
+
+        Classes expose a name to the current lexical environment. This treats
+        "class X {}" like "var X = class X {}". Ideally it would be "let X = class X {}".
+        Also, improve error messages for class statements where the class is missing a name.
+
+        * parser/Parser.h:
+        * parser/Parser.cpp:
+        (JSC::Parser<LexerType>::parseClass):
+        Fill name in info parameter if needed. Better error message if name is needed and missing.
+
+        (JSC::Parser<LexerType>::parseClassDeclaration):
+        Pass info parameter to get name, and expose the name as a variable name.
+
+        (JSC::Parser<LexerType>::parsePrimaryExpression):
+        Pass info parameter that is ignored.
+
+        * parser/ParserFunctionInfo.h:
+        Add a parser info for class, to extract the name.
+
 2015-03-25  Yusuke Suzuki  <utatane.tea@gmail.com>
 
         New map and set modification tests in r181922 fails
index ea615056eefb5debe2d7099d3703b5c18b9064f4..5f51da24b438539b7d5b0f495414909546272726 100644 (file)
@@ -1469,8 +1469,13 @@ template <class TreeBuilder> TreeStatement Parser<LexerType>::parseClassDeclarat
     JSTextPosition classStart = tokenStartPosition();
     unsigned classStartLine = tokenLine();
 
-    TreeClassExpression classExpr = parseClass(context, FunctionNeedsName);
+    ParserClassInfo<TreeBuilder> info;
+    TreeClassExpression classExpr = parseClass(context, FunctionNeedsName, info);
     failIfFalse(classExpr, "Failed to parse class");
+    declareVariable(info.className);
+
+    // FIXME: This should be like `let`, not `var`.
+    context.addVar(info.className, DeclarationStacks::HasInitializer);
 
     JSTextPosition classEnd = lastTokenEndPosition();
     unsigned classEndLine = tokenLine();
@@ -1479,7 +1484,7 @@ template <class TreeBuilder> TreeStatement Parser<LexerType>::parseClassDeclarat
 }
 
 template <typename LexerType>
-template <class TreeBuilder> TreeClassExpression Parser<LexerType>::parseClass(TreeBuilder& context, FunctionRequirements requirements)
+template <class TreeBuilder> TreeClassExpression Parser<LexerType>::parseClass(TreeBuilder& context, FunctionRequirements requirements, ParserClassInfo<TreeBuilder>& info)
 {
     ASSERT(match(CLASSTOKEN));
     JSTokenLocation location(tokenLocation());
@@ -1491,9 +1496,12 @@ template <class TreeBuilder> TreeClassExpression Parser<LexerType>::parseClass(T
     const Identifier* className = nullptr;
     if (match(IDENT)) {
         className = m_token.m_data.ident;
+        info.className = className;
         next();
         failIfFalse(classScope->declareVariable(className), "'", className->impl(), "' is not a valid class name");
     } else if (requirements == FunctionNeedsName) {
+        if (match(OPENBRACE))
+            semanticFail("Class statements must have a name");
         semanticFailureDueToKeyword("class name");
         failDueToUnexpectedToken();
     } else
@@ -2263,8 +2271,10 @@ template <class TreeBuilder> TreeExpression Parser<LexerType>::parsePrimaryExpre
         return context.createFunctionExpr(location, info);
     }
 #if ENABLE(ES6_CLASS_SYNTAX)
-    case CLASSTOKEN:
-        return parseClass(context, FunctionNoRequirements);
+    case CLASSTOKEN: {
+        ParserClassInfo<TreeBuilder> info;
+        return parseClass(context, FunctionNoRequirements, info);
+    }
 #endif
     case OPENBRACE:
         if (strictMode())
index 8bc47787188bc7ef9c6134b4979d454808c1a5f6..863f9c1560842a9856d8e97751cd8bedc8abd87a 100644 (file)
@@ -774,7 +774,7 @@ private:
 
     template <class TreeBuilder> NEVER_INLINE bool parseFunctionInfo(TreeBuilder&, FunctionRequirements, FunctionParseMode, bool nameIsInContainingScope, ConstructorKind, int functionKeywordStart, ParserFunctionInfo<TreeBuilder>&);
 #if ENABLE(ES6_CLASS_SYNTAX)
-    template <class TreeBuilder> NEVER_INLINE TreeClassExpression parseClass(TreeBuilder&, FunctionRequirements);
+    template <class TreeBuilder> NEVER_INLINE TreeClassExpression parseClass(TreeBuilder&, FunctionRequirements, ParserClassInfo<TreeBuilder>&);
 #endif
 
     ALWAYS_INLINE int isBinaryOperator(JSTokenType);
index e20824541a30616df5b9e2b1677da079e9be77a5..6b736282e2d2a81bc63dc2f3c0e5ae8c858cd1f8 100644 (file)
@@ -40,6 +40,13 @@ struct ParserFunctionInfo {
     unsigned bodyStartColumn = 0;
 };
 
+#if ENABLE(ES6_CLASS_SYNTAX)
+template <class TreeBuilder>
+struct ParserClassInfo {
+    const Identifier* className = 0;
+};
+#endif
+
 }
 
 #endif