branch: master commit c79d4260a38a5b44cd52dabfdccbff12323870ac Author: Stephen Hicks <s...@google.com> Commit: Stephen Hicks <s...@google.com>
Support ES6 class statements/expressions. This is specified in ยง14.5 of the draft ES6 spec: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-class-definitions, and described via examples at https://github.com/lukehoban/es6features#enhanced-object-literals. This commit does not yet add support for the 'static' keyword. --- js2-mode.el | 142 ++++++++++++++++++++++++++++++++++++++++++++++++------ tests/parser.el | 32 ++++++++++++ 2 files changed, 158 insertions(+), 16 deletions(-) diff --git a/js2-mode.el b/js2-mode.el index c53c5b6..64b20f8 100644 --- a/js2-mode.el +++ b/js2-mode.el @@ -661,8 +661,10 @@ which doesn't seem particularly useful, but Rhino permits it." (defvar js2-ENUM 161) ; for "enum" reserved word (defvar js2-TRIPLEDOT 162) ; for rest parameter (defvar js2-ARROW 163) ; function arrow (=>) +(defvar js2-CLASS 164) +(defvar js2-EXTENDS 165) -(defconst js2-num-tokens (1+ js2-ARROW)) +(defconst js2-num-tokens (1+ js2-EXTENDS)) (defconst js2-debug-print-trees nil) @@ -1956,6 +1958,18 @@ the correct number of ARGS must be provided." "Yield from closing generator") ;; Classes +(js2-msg "msg.unnamed.class.stmt" ; added by js2-mode + "class statement requires a name") + +(js2-msg "msg.class.unexpected.comma" ; added by js2-mode + "unexpected ',' between class properties") + +(js2-msg "msg.missing.extends" ; added by js2-mode + "name is required after extends") + +(js2-msg "msg.no.brace.class" ; added by js2-mode + "missing '{' before class body") + (js2-msg "msg.missing.computed.rb" ; added by js2-mode "missing ']' after computed property expression") @@ -3494,6 +3508,49 @@ You can tell the quote type by looking at the first character." (insert ","))) (insert "]")) +(defstruct (js2-class-node + (:include js2-node) + (:constructor nil) + (:constructor make-js2-class-node (&key (type js2-CLASS) + (pos js2-ts-cursor) + (form 'CLASS_STATEMENT) + (name "") + extends len elems))) + "AST node for an class expression. +`elems' is a list of `js2-object-prop-node', and `extends' is an +optional `js2-expr-node'" + form ; CLASS_{STATEMENT|EXPRESSION} + name ; class name (a `js2-node-name', or nil if anonymous) + extends ; class heritage (a `js2-expr-node', or nil if none) + elems) + +(put 'cl-struct-js2-class-node 'js2-visitor 'js2-visit-class-node) +(put 'cl-struct-js2-class-node 'js2-printer 'js2-print-class-node) + +(defun js2-visit-class-node (n v) + (js2-visit-ast (js2-class-node-name n) v) + (js2-visit-ast (js2-class-node-extends n) v) + (dolist (e (js2-class-node-elems n)) + (js2-visit-ast e v))) + +(defun js2-print-class-node (n i) + (let* ((pad (js2-make-pad i)) + (name (js2-class-node-name n)) + (extends (js2-class-node-extends n)) + (elems (js2-class-node-elems n))) + (insert pad "class") + (when name + (insert " ") + (js2-print-ast name 0)) + (when extends + (insert " extends ") + (js2-print-ast extends)) + (insert " {") + (dolist (elem elems) + (insert "\n") + (js2-print-ast elem (1+ i))) + (insert "\n" pad "}"))) + (defstruct (js2-object-node (:include js2-node) (:constructor nil) @@ -4660,6 +4717,7 @@ You should use `js2-print-tree' instead of this function." js2-CALL js2-CATCH js2-CATCH_SCOPE + js2-CLASS js2-CONST js2-CONTINUE js2-DEBUGGER @@ -5283,9 +5341,9 @@ into temp buffers." (defconst js2-keywords '(break - case catch const continue + case catch class const continue debugger default delete do - else enum + else enum extends false finally for function if in instanceof import let @@ -5303,9 +5361,9 @@ into temp buffers." (let ((table (make-vector js2-num-tokens nil)) (tokens (list js2-BREAK - js2-CASE js2-CATCH js2-CONST js2-CONTINUE + js2-CASE js2-CATCH js2-CLASS js2-CONST js2-CONTINUE js2-DEBUGGER js2-DEFAULT js2-DELPROP js2-DO - js2-ELSE + js2-ELSE js2-EXTENDS js2-FALSE js2-FINALLY js2-FOR js2-FUNCTION js2-IF js2-IN js2-INSTANCEOF js2-IMPORT js2-LET @@ -6576,7 +6634,8 @@ of a simple name. Called before EXPR has a parent node." "Highlight function properties and external variables." (let (leftpos name) ;; highlight vars and props assigned function values - (when (js2-function-node-p right) + (when (or (js2-function-node-p right) + (js2-class-node-p right)) (cond ;; var foo = function() {...} ((js2-name-node-p left) @@ -7615,6 +7674,7 @@ node are given relative start positions and correct lengths." (let ((parsers (make-vector js2-num-tokens #'js2-parse-expr-stmt))) (aset parsers js2-BREAK #'js2-parse-break) + (aset parsers js2-CLASS #'js2-parse-class-stmt) (aset parsers js2-CONST #'js2-parse-const-var) (aset parsers js2-CONTINUE #'js2-parse-continue) (aset parsers js2-DEBUGGER #'js2-parse-debugger) @@ -7660,6 +7720,7 @@ node are given relative start positions and correct lengths." js2-LC js2-ERROR js2-SEMI + js2-CLASS js2-FUNCTION) "List of tokens that don't do automatic semicolon insertion.") @@ -9239,6 +9300,8 @@ array-literals, array comprehensions and regular expressions." tt) (setq tt (js2-current-token-type)) (cond + ((= tt js2-CLASS) + (js2-parse-class-expr)) ((= tt js2-FUNCTION) (js2-parse-function-expr)) ((= tt js2-LB) @@ -9540,7 +9603,52 @@ If ONLY-OF-P is non-nil, only the 'for (foo of bar)' form is allowed." (js2-node-add-children pn iter obj) pn)) +(defun js2-parse-class-stmt () + (let ((pos (js2-current-token-beg))) + (js2-must-match-name "msg.unnamed.class.stmt") + (js2-parse-class pos 'CLASS_STATEMENT (js2-create-name-node t)))) + +(defun js2-parse-class-expr () + (let ((pos (js2-current-token-beg)) + name) + (when (js2-match-token js2-NAME) + (setq name (js2-create-name-node t))) + (js2-parse-class pos 'CLASS_EXPRESSION name))) + +(defun js2-parse-class (pos form name) + ;; class X [extends ...] { + (let (pn elems extends) + (when name + (js2-set-face (js2-node-pos name) (js2-node-end name) + 'font-lock-function-name-face 'record)) + (if (js2-match-token js2-EXTENDS) + (if (= (js2-peek-token) js2-LC) + (js2-report-error "msg.missing.extends") + ;; TODO(sdh): this should be left-hand-side-expr, not assign-expr + (setq extends (js2-parse-assign-expr)) + (if (not extends) + (js2-report-error "msg.bad.extends")))) + (js2-must-match js2-LC "msg.no.brace.class") + (setq elems (js2-parse-object-literal-elems t) + pn (make-js2-class-node :pos pos + :len (- js2-ts-cursor pos) + :form form + :name name + :extends extends + :elems elems)) + (apply #'js2-node-add-children pn (js2-class-node-elems pn)) + pn)) + (defun js2-parse-object-literal () + (let* ((pos (js2-current-token-beg)) + (elems (js2-parse-object-literal-elems)) + (result (make-js2-object-node :pos pos + :len (- js2-ts-cursor pos) + :elems elems))) + (apply #'js2-node-add-children result (js2-object-node-elems result)) + result)) + +(defun js2-parse-object-literal-elems (&optional class-p) (let ((pos (js2-current-token-beg)) tt elems result after-comma (continue t)) @@ -9569,8 +9677,9 @@ If ONLY-OF-P is non-nil, only the 'for (foo of bar)' form is allowed." ((= tt js2-NUMBER) (setq after-comma nil) (push (js2-parse-plain-property (make-js2-number-node)) elems)) - ;; trailing comma - ((= tt js2-RC) + ;; break out of loop, trailing comma + ((or (= tt js2-RC) + (= tt js2-EOF)) (js2-unget-token) (setq continue nil) (if after-comma @@ -9580,15 +9689,16 @@ If ONLY-OF-P is non-nil, only the 'for (foo of bar)' form is allowed." (js2-report-error "msg.bad.prop") (unless js2-recover-from-parse-errors (setq continue nil)))) ; end switch - (if (js2-match-token js2-COMMA) - (setq after-comma (js2-current-token-end)) - (setq continue nil))) ; end loop + ;; handle commas, depending on class-p + (let ((comma (js2-match-token js2-COMMA))) + (if class-p + (if comma + (js2-report-error "msg.class.unexpected.comma")) + (if comma + (setq after-comma (js2-current-token-end)) + (setq continue nil))))) ; end loop (js2-must-match js2-RC "msg.no.brace.prop") - (setq result (make-js2-object-node :pos pos - :len (- js2-ts-cursor pos) - :elems (nreverse elems))) - (apply #'js2-node-add-children result (js2-object-node-elems result)) - result)) + (nreverse elems))) (defun js2-parse-named-prop (tt) "Parse a name, string, or getter/setter object property. diff --git a/tests/parser.el b/tests/parser.el index fe66d36..65f3ae4 100644 --- a/tests/parser.el +++ b/tests/parser.el @@ -332,6 +332,38 @@ the test." (js2-deftest-parse octal-number-broken "0o812;" :syntax-error "0o8" :errors-count 2) +;;; Classes + +(js2-deftest-parse parse-harmony-class-statement + "class Foo {\n get bar() { return 42;\n}\n set bar(x) { y = x;\n}\n}") + +(js2-deftest-parse parse-harmony-class-statement-without-name-is-not-ok + "class {\n get bar() { return 42;\n}\n}" + :syntax-error "{") + +(js2-deftest-parse parse-harmony-class-expression + "var Foo1 = class Foo {\n bar() { return 42;\n}\n};") + +(js2-deftest-parse parse-harmony-anonymous-class-expression + "var Foo = class {\n set bar(x) { bar = x;\n}\n};") + +(js2-deftest-parse parse-harmony-class-with-extends + "class Foo extends Bar {\n}") + +(js2-deftest-parse parse-harmony-anonymous-class-with-extends + "foo.Foo = class extends Bar {\n set bar(x) { bar = x;\n}\n};") + +(js2-deftest-parse parse-harmony-class-with-complex-extends + "class Foo extends foo[BAR][2].Baz {\n}") + +(js2-deftest-parse parse-harmony-class-missing-extended-class-is-not-ok + "class Foo extends {\n}" + :syntax-error "extends") + +(js2-deftest-parse parse-unterminated-class-is-not-okay + "class Foo {\n get bar() { return 42;\n}" + :syntax-error "}") + ;;; Scopes (js2-deftest ast-symbol-table-includes-fn-node "function foo() {}"