From 58c6636d0b3b7308b8c00bb6739248da6f5b05ff Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Mon, 29 Dec 2014 03:06:12 +0200 Subject: [PATCH] Support ES6 module syntax Closes #175 --- js2-mode.el | 556 ++++++++++++++++++++++++++++++++++++++++++++++- tests/externs.el | 4 + tests/parser.el | 270 +++++++++++++++++++++++ 3 files changed, 826 insertions(+), 4 deletions(-) diff --git a/js2-mode.el b/js2-mode.el index 91b306244..5bcbea553 100644 --- a/js2-mode.el +++ b/js2-mode.el @@ -1495,6 +1495,30 @@ the correct number of ARGS must be provided." (js2-msg "msg.let.decl.not.in.block" "SyntaxError: let declaration not directly within block") +(js2-msg "msg.mod.import.decl.at.top.level" + "SyntaxError: import declarations may only appear at the top level") + +(js2-msg "msg.mod.as.after.reserved.word" + "SyntaxError: missing keyword 'as' after reserved word %s") + +(js2-msg "msg.mod.rc.after.import.spec.list" + "SyntaxError: missing '}' after module specifier list") + +(js2-msg "msg.mod.from.after.import.spec.set" + "SyntaxError: missing keyword 'from' after import specifier set") + +(js2-msg "msg.mod.declaration.after.import" + "SyntaxError: missing declaration after 'import' keyword") + +(js2-msg "msg.mod.spec.after.from" + "SyntaxError: missing module specifier after 'from' keyword") + +(js2-msg "msg.mod.export.decl.at.top.level" + "SyntaxError: export declarations may only appear at top level") + +(js2-msg "msg.mod.rc.after.export.spec.list" + "SyntaxError: missing '}' after export specifier list") + ;; NodeTransformer (js2-msg "msg.dup.label" "duplicated label") @@ -2478,6 +2502,66 @@ NAME can be a Lisp symbol or string. SYMBOL is a `js2-symbol'." (js2-print-ast (js2-do-node-condition n) 0) (insert ");\n"))) +(defstruct (js2-export-node + (:include js2-node) + (:constructor nil) + (:constructor make-js2-export-node (&key (type js2-EXPORT) + (pos) (js2-current-token-beg) + len + exports-list + from-clause + declaration + default))) + "AST node for an export statement. There are many things that can be exported, +so many of its properties will be nil. +" + exports-list ; lisp list of js2-export-binding-node to export + from-clause ; js2-from-clause-node for re-exporting symbols from another module + declaration ; js2-var-decl-node (var, let, const) or js2-class-node + default) ; js2-function-node or js2-assign-node + +(put 'cl-struct-js2-export-node 'js2-visitor 'js2-visit-export-node) +(put 'cl-struct-js2-export-node 'js2-printer 'js2-print-export-node) + +(defun js2-visit-export-node (n v) + (let ((exports-list (js2-export-node-exports-list n)) + (from (js2-export-node-from-clause n)) + (declaration (js2-export-node-declaration n)) + (default (js2-export-node-default n))) + (when exports-list + (dolist (export exports-list) + (js2-visit-ast export v))) + (when from + (js2-visit-ast from v)) + (when declaration + (js2-visit-ast declaration v)) + (when default + (js2-visit-ast default v)))) + +(defun js2-print-export-node (n i) + (let ((pad (js2-make-pad i)) + (exports-list (js2-export-node-exports-list n)) + (from (js2-export-node-from-clause n)) + (declaration (js2-export-node-declaration n)) + (default (js2-export-node-default n))) + (insert pad "export ") + (cond + (default + (insert "default ") + (js2-print-ast default i)) + (declaration + (js2-print-ast declaration i)) + ((and exports-list from) + (js2-print-named-imports exports-list) + (insert " ") + (js2-print-from-clause from)) + (from + (insert "* ") + (js2-print-from-clause from)) + (exports-list + (js2-print-named-imports exports-list))) + (insert ";\n"))) + (defstruct (js2-while-node (:include js2-loop-node) (:constructor nil) @@ -2650,6 +2734,204 @@ NAME can be a Lisp symbol or string. SYMBOL is a `js2-symbol'." (js2-print-body else-part (1+ i)) (insert pad "}\n"))))) +(defstruct (js2-export-binding-node + (:include js2-node) + (:constructor nil) + (:constructor make-js2-export-binding-node (&key (type -1) + pos + len + local-name + extern-name))) + "AST node for an external symbol binding. +It contains a local-name node which is the name of the value in the +current scope, and extern-name which is the name of the value in the +imported or exported scope. By default these are the same, but if the +name is aliased as in {foo as bar}, it would have an extern-name node +containing 'foo' and a local-name node containing 'bar'." + local-name ; js2-name-node with the variable name in this scope + extern-name) ; js2-name-node with the value name in the exporting module + +(put 'cl-struct-js2-export-binding-node 'js2-printer 'js2-print-extern-binding) +(put 'cl-struct-js2-export-binding-node 'js2-visitor 'js2-visit-extern-binding) + +(defun js2-visit-extern-binding (n v) + "Visit an extern binding node. First visit the local-name, and, if +different, visit the extern-name." + (let ((local-name (js2-export-binding-node-local-name n)) + (extern-name (js2-export-binding-node-extern-name n))) + (when local-name + (js2-visit-ast local-name v)) + (when (not (equal local-name extern-name)) + (js2-visit-ast extern-name v)))) + +(defun js2-print-extern-binding (n i) + "Print a representation of a single extern binding. E.g. 'foo' or +'foo as bar'." + (let ((local-name (js2-export-binding-node-local-name n)) + (extern-name (js2-export-binding-node-extern-name n))) + (insert (js2-name-node-name extern-name)) + (when (not (equal local-name extern-name)) + (insert " as ") + (insert (js2-name-node-name local-name))))) + + +(defstruct (js2-import-node + (:include js2-node) + (:constructor nil) + (:constructor make-js2-import-node (&key (type js2-IMPORT) + (pos (js2-current-token-beg)) + len + import + from + module-id))) + "AST node for an import statement. It follows the form + +import ModuleSpecifier; +import ImportClause FromClause;" + import ; js2-import-clause-node specifying which names are to imported. + from ; js2-from-clause-node indicating the module from which to import. + module-id) ; module-id of the import. E.g. 'src/mylib'. + +(put 'cl-struct-js2-import-node 'js2-printer 'js2-print-import) +(put 'cl-struct-js2-import-node 'js2-visitor 'js2-visit-import) + +(defun js2-visit-import (n v) + (let ((import-clause (js2-import-node-import n)) + (from-clause (js2-import-node-from n))) + (when import-clause + (js2-visit-ast import-clause v)) + (when from-clause + (js2-visit-ast from-clause v)))) + +(defun js2-print-import (n i) + "Prints a representation of the import node" + (let ((pad (js2-make-pad i)) + (import-clause (js2-import-node-import n)) + (from-clause (js2-import-node-from n)) + (module-id (js2-import-node-module-id n))) + (insert pad "import ") + (if import-clause + (progn + (js2-print-import-clause import-clause) + (insert " ") + (js2-print-from-clause from-clause)) + (insert "'") + (insert module-id) + (insert "'")) + (insert ";\n"))) + +(defstruct (js2-import-clause-node + (:include js2-node) + (:constructor nil) + (:constructor make-js2-import-clause-node (&key (type -1) + pos + len + namespace-import + named-imports + default-binding))) + "AST node corresponding to the import clause of an import statement. This is +the portion of the import that bindings names from the external context to the +local context." + namespace-import ; js2-namespace-import-node. E.g. '* as lib' + named-imports ; lisp list of js2-export-binding-node for all named imports. + default-binding) ; js2-export-binding-node for the default import binding + +(put 'cl-struct-js2-import-clause-node 'js2-visitor 'js2-visit-import-clause) +(put 'cl-struct-js2-import-clause-node 'js2-printer 'js2-print-import-clause) + +(defun js2-visit-import-clause (n v) + (let ((ns-import (js2-import-clause-node-namespace-import n)) + (named-imports (js2-import-clause-node-named-imports n)) + (default (js2-import-clause-node-default-binding n))) + (when ns-import + (js2-visit-ast ns-import v)) + (when named-imports + (dolist (import named-imports) + (js2-visit-ast import v))) + (when default + (js2-visit-ast default v)))) + +(defun js2-print-import-clause (n) + (let ((ns-import (js2-import-clause-node-namespace-import n)) + (named-imports (js2-import-clause-node-named-imports n)) + (default (js2-import-clause-node-default-binding n))) + (cond + ((and default ns-import) + (js2-print-ast default) + (insert ", ") + (js2-print-namespace-import ns-import)) + ((and default named-imports) + (js2-print-ast default) + (insert ", ") + (js2-print-named-imports named-imports)) + (default + (js2-print-ast default)) + (ns-import + (js2-print-namespace-import ns-import)) + (named-imports + (js2-print-named-imports named-imports))))) + +(defun js2-print-namespace-import (node) + (insert "* as ") + (insert (js2-name-node-name (js2-namespace-import-node-name node)))) + +(defun js2-print-named-imports (imports) + (insert "{") + (let ((len (length imports)) + (n 0)) + (while (< n len) + (js2-print-extern-binding (nth n imports) 0) + (unless (= n (- len 1)) + (insert ", ")) + (setq n (+ n 1)))) + (insert "}")) + +(defstruct (js2-namespace-import-node + (:include js2-node) + (:constructor nil) + (:constructor make-js2-namespace-import-node (&key (type -1) + pos + len + name))) + "AST node for a complete namespace import. +E.g. the '* as lib' expression in: + +import * as lib from 'src/lib' + +It contains a single name node referring to the bound name." + name) ; js2-name-node of the bound name. + +(defun js2-visit-namespace-import (n v) + (js2-visit-ast (js2-namespace-import-node-name n) v)) + +(put 'cl-struct-js2-namespace-import-node 'js2-visitor 'js2-visit-namespace-import) +(put 'cl-struct-js2-namespace-import-node 'js2-printer 'js2-print-namespace-import) + +(defstruct (js2-from-clause-node + (:include js2-node) + (:constructor nil) + (:constructor make-js2-from-clause-node (&key (type js2-NAME) + pos + len + module-id + metadata-p))) + "AST node for the from clause in an import or export statement. +E.g. from 'my/module'. It can refere to either an external module, or to the +modules metadata itself." + module-id ; string containing the module specifier. + metadata-p) ; true if this clause refers to the module's metadata + +(put 'cl-struct-js2-from-clause-node 'js2-visitor 'js2-visit-none) +(put 'cl-struct-js2-from-clause-node 'js2-printer 'js2-print-from-clause) + +(defun js2-print-from-clause (n) + (insert "from ") + (if (js2-from-clause-node-metadata-p n) + (insert "this module") + (insert "'") + (insert (js2-from-clause-node-module-id n)) + (insert "'"))) + (defstruct (js2-try-node (:include js2-node) (:constructor nil) @@ -4861,10 +5143,12 @@ You should use `js2-print-tree' instead of this function." js2-CONTINUE js2-DEFAULT ; e4x "default xml namespace" statement js2-DO + js2-EXPORT js2-EXPR_RESULT js2-EXPR_VOID js2-FOR js2-IF + js2-IMPORT js2-RETURN js2-SWITCH js2-THROW @@ -5395,7 +5679,7 @@ into temp buffers." '(break case catch class const continue debugger default delete do - else extends + else extends export false finally for function if in instanceof import let @@ -5415,7 +5699,8 @@ into temp buffers." (list js2-BREAK js2-CASE js2-CATCH js2-CLASS js2-CONST js2-CONTINUE js2-DEBUGGER js2-DEFAULT js2-DELPROP js2-DO - js2-ELSE js2-EXTENDS + js2-ELSE js2-EXPORT + js2-ELSE js2-EXTENDS js2-EXPORT js2-FALSE js2-FINALLY js2-FOR js2-FUNCTION js2-IF js2-IN js2-INSTANCEOF js2-IMPORT js2-LET @@ -7760,9 +8045,11 @@ node are given relative start positions and correct lengths." (aset parsers js2-DEBUGGER #'js2-parse-debugger) (aset parsers js2-DEFAULT #'js2-parse-default-xml-namespace) (aset parsers js2-DO #'js2-parse-do) + (aset parsers js2-EXPORT #'js2-parse-export) (aset parsers js2-FOR #'js2-parse-for) (aset parsers js2-FUNCTION #'js2-parse-function-stmt) (aset parsers js2-IF #'js2-parse-if) + (aset parsers js2-IMPORT #'js2-parse-import) (aset parsers js2-LC #'js2-parse-block) (aset parsers js2-LET #'js2-parse-let-stmt) (aset parsers js2-NAME #'js2-parse-name-or-label) @@ -7887,6 +8174,215 @@ Return value is a list (EXPR LP RP), with absolute paren positions." (js2-node-add-children pn (car cond) if-true if-false) pn)) +(defun js2-parse-import () + "Parse import statement. The current token must be js2-IMPORT." + (unless (js2-ast-root-p js2-current-scope) + (js2-report-error "msg.mod.import.decl.at.top.level")) + (let ((beg (js2-current-token-beg))) + (cond ((js2-match-token js2-STRING) + (make-js2-import-node + :pos beg + :len (- (js2-current-token-end) beg) + :module-id (js2-current-token-string))) + (t + (let* ((import-clause (js2-parse-import-clause)) + (from-clause (and import-clause (js2-parse-from-clause))) + (module-id (when from-clause (js2-from-clause-node-module-id from-clause))) + (node (make-js2-import-node + :pos beg + :len (- (js2-current-token-end) beg) + :import import-clause + :from from-clause + :module-id module-id))) + (when import-clause + (js2-node-add-children node import-clause)) + (when from-clause + (js2-node-add-children node from-clause)) + node))))) + +(defun js2-parse-import-clause () + "Parse the bindings in an import statement. +This can take many forms: + +ImportedDefaultBinding -> 'foo' +NameSpaceImport -> '* as lib' +NamedImports -> '{foo as bar, bang}' +ImportedDefaultBinding , NameSpaceImport -> 'foo, * as lib' +ImportedDefaultBinding , NamedImports -> 'foo, {bar, baz as bif}' + +Try to match namespace imports and named imports first because nothing can +come after them. If it is an imported default binding, then it could have named +imports or a namespace import that follows it. +" + (let* ((beg (js2-current-token-beg)) + (clause (make-js2-import-clause-node + :pos beg)) + (children (list))) + (cond + ((js2-match-token js2-MUL) + (let ((ns-import (js2-parse-namespace-import))) + (when ns-import + (let ((name-node (js2-namespace-import-node-name ns-import))) + (js2-define-symbol + js2-LET (js2-name-node-name name-node) name-node t))) + (setf (js2-import-clause-node-namespace-import clause) ns-import) + (push ns-import children))) + ((js2-match-token js2-LC) + (let ((imports (js2-parse-export-bindings t))) + (setf (js2-import-clause-node-named-imports clause) imports) + (dolist (import imports) + (push import children) + (let ((name-node (js2-export-binding-node-local-name import))) + (when name-node + (js2-define-symbol + js2-LET (js2-name-node-name name-node) name-node t)))))) + ((= (js2-peek-token) js2-NAME) + (let ((binding (js2-maybe-parse-export-binding))) + (let ((node-name (js2-export-binding-node-local-name binding))) + (js2-define-symbol js2-LET (js2-name-node-name node-name) node-name t)) + (setf (js2-import-clause-node-default-binding clause) binding) + (push binding children)) + (when (js2-match-token js2-COMMA) + (cond + ((js2-match-token js2-MUL) + (let ((ns-import (js2-parse-namespace-import))) + (let ((name-node (js2-namespace-import-node-name ns-import))) + (js2-define-symbol + js2-LET (js2-name-node-name name-node) name-node t)) + (setf (js2-import-clause-node-namespace-import clause) ns-import) + (push ns-import children))) + ((js2-match-token js2-LC) + (let ((imports (js2-parse-export-bindings t))) + (setf (js2-import-clause-node-named-imports clause) imports) + (dolist (import imports) + (push import children) + (let ((name-node (js2-export-binding-node-local-name import))) + (when name-node + (js2-define-symbol + js2-LET (js2-name-node-name name-node) name-node t)))))) + (t (js2-report-error "msg.syntax"))))) + (t (js2-report-error "msg.mod.declaration.after.import"))) + (setf (js2-node-len clause) (- (js2-current-token-end) beg)) + (apply #'js2-node-add-children clause children) + clause)) + +(defun js2-parse-namespace-import () + "Parse a namespace import expression such as '* as bar'. +The current token must be js2-MUL." + (let ((beg (js2-current-token-beg))) + (when (js2-must-match js2-NAME "msg.syntax") + (if (equal "as" (js2-current-token-string)) + (when (js2-must-match-prop-name "msg.syntax") + (let ((node (make-js2-namespace-import-node + :pos beg + :len (- (js2-current-token-end) beg) + :name (make-js2-name-node + :pos (js2-current-token-beg) + :len (js2-current-token-end) + :name (js2-current-token-string))))) + (js2-node-add-children node (js2-namespace-import-node-name node)) + node)) + (js2-unget-token) + (js2-report-error "msg.syntax"))))) + + +(defun js2-parse-from-clause () + "Parse the from clause in an import or export statement. E.g. from 'src/lib'" + (when (js2-must-match-name "msg.mod.from.after.import.spec.set") + (let ((beg (js2-current-token-beg))) + (if (equal "from" (js2-current-token-string)) + (cond + ((js2-match-token js2-STRING) + (make-js2-from-clause-node + :pos beg + :len (- (js2-current-token-end) beg) + :module-id (js2-current-token-string) + :metadata-p nil)) + ((js2-match-token js2-THIS) + (when (js2-must-match-name "msg.mod.spec.after.from") + (if (equal "module" (js2-current-token-string)) + (make-js2-from-clause-node + :pos beg + :len (- (js2-current-token-end) beg) + :module-id "this" + :metadata-p t) + (js2-unget-token) + (js2-unget-token) + (js2-report-error "msg.mod.spec.after.from") + nil))) + (t (js2-report-error "msg.mod.spec.after.from") nil)) + (js2-unget-token) + (js2-report-error "msg.mod.from.after.import.spec.set") + nil)))) + +(defun js2-parse-export-bindings (&optional import-p) + "Parse a list of export binding expressions such as {}, {foo, bar}, and +{foo as bar, baz as bang}. The current token must be +js2-LC. Return a lisp list of js2-export-binding-node" + (let ((bindings (list))) + (while + (let ((binding (js2-maybe-parse-export-binding))) + (when binding + (push binding bindings)) + (js2-match-token js2-COMMA))) + (when (js2-must-match js2-RC (if import-p + "msg.mod.rc.after.import.spec.list" + "msg.mod.rc.after.export.spec.list")) + (reverse bindings)))) + +(defun js2-maybe-parse-export-binding () + "Attempt to parse a binding expression found inside an import/export statement. +This can take the form of either as single js2-NAME token as in 'foo' or as in a +rebinding expression 'bar as foo'. If it matches, it will return an instance of +js2-export-binding-node and consume all the tokens. If it does not match, it +consumes no tokens." + (let ((extern-name (when (js2-match-prop-name) (js2-current-token-string))) + (beg (js2-current-token-beg)) + (extern-name-len (js2-current-token-len)) + (is-reserved-name (or (= (js2-current-token-type) js2-RESERVED) + (aref js2-kwd-tokens (js2-current-token-type))))) + (if extern-name + (let ((as (and (js2-match-token js2-NAME) (js2-current-token-string)))) + (if (and as (equal "as" (js2-current-token-string))) + (let ((name + (or + (and (js2-match-token js2-DEFAULT) "default") + (and (js2-match-token js2-NAME) (js2-current-token-string))))) + (if name + (let ((node (make-js2-export-binding-node + :pos beg + :len (- (js2-current-token-end) beg) + :local-name (make-js2-name-node + :name name + :pos (js2-current-token-beg) + :len (js2-current-token-len)) + :extern-name (make-js2-name-node + :name extern-name + :pos beg + :len extern-name-len)))) + (js2-node-add-children + node + (js2-export-binding-node-local-name node) + (js2-export-binding-node-extern-name node)) + node) + (js2-unget-token) + nil)) + (when as (js2-unget-token)) + (let* ((name-node (make-js2-name-node + :name (js2-current-token-string) + :pos (js2-current-token-beg) + :len (js2-current-token-len))) + (node (make-js2-export-binding-node + :pos (js2-current-token-beg) + :len (js2-current-token-len) + :local-name name-node + :extern-name name-node))) + (when is-reserved-name + (js2-report-error "msg.mod.as.after.reserved.word" extern-name)) + (js2-node-add-children node name-node) + node))) + nil))) + (defun js2-parse-switch () "Parser for switch-statement. Last matched token must be js2-SWITCH." (let ((pos (js2-current-token-beg)) @@ -7990,6 +8486,56 @@ Return value is a list (EXPR LP RP), with absolute paren positions." (setf (js2-node-len pn) (- end pos)) pn)) +(defun js2-parse-export () + "Parse an export statement. +The Last matched token must be js2-EXPORT. Currently, the 'default' and 'expr' +expressions should only be either hoistable expressions (function or generator) +or assignment expressions, but there is no checking to enforce that and so it +will parse without error a small subset of +invalid export statements." + (unless (js2-ast-root-p js2-current-scope) + (js2-report-error "msg.mod.export.decl.at.top.level")) + (let ((beg (js2-current-token-beg)) + (children (list)) + exports-list from-clause declaration default) + (cond + ((js2-match-token js2-MUL) + (setq from-clause (js2-parse-from-clause)) + (when from-clause + (push from-clause children))) + ((js2-match-token js2-LC) + (setq exports-list (js2-parse-export-bindings)) + (when exports-list + (dolist (export exports-list) + (push export children))) + (when (js2-match-token js2-NAME) + (if (equal "from" (js2-current-token-string)) + (progn + (js2-unget-token) + (setq from-clause (js2-parse-from-clause))) + (js2-unget-token)))) + ((js2-match-token js2-DEFAULT) + (setq default (js2-parse-expr))) + ((or (js2-match-token js2-VAR) (js2-match-token js2-CONST) (js2-match-token js2-LET)) + (setq declaration (js2-parse-variables (js2-current-token-type) (js2-current-token-beg)))) + (t + (setq declaration (js2-parse-expr)))) + (when from-clause + (push from-clause children)) + (when declaration + (push declaration children)) + (when default + (push default children)) + (let ((node (make-js2-export-node + :pos beg + :len (- (js2-current-token-end) beg) + :exports-list exports-list + :from-clause from-clause + :declaration declaration + :default default))) + (apply #'js2-node-add-children node children) + node))) + (defun js2-parse-for () "Parse a for, for-in or for each-in statement. Last matched token must be js2-FOR." @@ -8647,7 +9193,9 @@ If NODE is non-nil, it is the AST node associated with the symbol." (let* ((defining-scope (js2-get-defining-scope js2-current-scope name)) (symbol (if defining-scope (js2-scope-get-symbol defining-scope name))) - (sdt (if symbol (js2-symbol-decl-type symbol) -1))) + (sdt (if symbol (js2-symbol-decl-type symbol) -1)) + (pos (if node (js2-node-abs-pos node))) + (len (if node (js2-node-len node)))) (cond ((and symbol ; already defined (or (= sdt js2-CONST) ; old version is const @@ -8662,7 +9210,7 @@ If NODE is non-nil, it is the AST node associated with the symbol." ((= sdt js2-VAR) "msg.var.redecl") ((= sdt js2-FUNCTION) "msg.function.redecl") (t "msg.parm.redecl")) - name)) + name pos len)) ((= decl-type js2-LET) (if (and (not ignore-not-in-block) (or (= (js2-node-type js2-current-scope) js2-IF) diff --git a/tests/externs.el b/tests/externs.el index 8fb116113..40957f600 100644 --- a/tests/externs.el +++ b/tests/externs.el @@ -41,3 +41,7 @@ (js2-mode) (should (equal (js2-get-jslint-globals) '("quux" "tee" "$"))))) + +;;;TODO +;; ensure that any symbols bound with the import syntax are added to the extern list +;; ensure that any symbols bound with the export syntax exist in the file scope diff --git a/tests/parser.el b/tests/parser.el index 00bd2cff9..2b229eb6d 100644 --- a/tests/parser.el +++ b/tests/parser.el @@ -345,6 +345,276 @@ the test." (js2-deftest-parse octal-number-broken "0o812;" :syntax-error "0o8" :errors-count 2) +;;; Modules + +(js2-deftest parse-export-bindings "{one, two as dos}" + (js2-init-scanner) + (should (js2-match-token js2-LC)) + (let ((imports (js2-parse-export-bindings))) + (should (not (equal nil imports))) + (should (= 2 (length imports))) + (let ((first (nth 0 imports)) + (second (nth 1 imports))) + (should (equal "one" (js2-name-node-name (js2-export-binding-node-extern-name first)))) + (should (equal "two" (js2-name-node-name (js2-export-binding-node-extern-name second)))) + (let ((first-name (js2-export-binding-node-local-name first)) + (second-name (js2-export-binding-node-local-name second))) + (should (equal first (js2-node-parent first-name))) + (should (equal 3 (js2-node-len first-name))) + (should (equal "one" (js2-name-node-name first-name))) + (should (equal second (js2-node-parent second-name))) + (should (equal 3 (js2-node-len second-name))) + (should (equal "dos" (js2-name-node-name second-name))))))) + +(js2-deftest parse-export-binding-as-default "one as default" + (js2-init-scanner) + (let ((binding (js2-maybe-parse-export-binding))) + (should binding) + (should (js2-export-binding-node-p binding)) + (let ((name (js2-export-binding-node-local-name binding))) + (should name) + (should (equal "default" (js2-name-node-name name)))))) + +(js2-deftest parse-namepsace-import "* as lib;" + (js2-init-scanner) + (should (js2-match-token js2-MUL)) + (let ((namespace-import (js2-parse-namespace-import))) + (should (not (equal nil namespace-import))) + (should (js2-namespace-import-node-p namespace-import)) + (should (= 1 (js2-node-pos namespace-import))) + (should (equal 8 (js2-node-len namespace-import))) + (let ((name-node (js2-namespace-import-node-name namespace-import))) + (should (equal "lib" (js2-name-node-name name-node))) + (should (= 5 (js2-node-pos name-node)))))) + +(js2-deftest parse-from-clause "from 'foo/bar';" + (js2-init-scanner) + (let ((from (js2-parse-from-clause))) + (should (not (equal nil from))) + (should (= 1 (js2-node-pos from))) + (should (= 14 (js2-node-len from))) + (should (equal "foo/bar" (js2-from-clause-node-module-id from))))) + +(js2-deftest parse-import-module-id-only "import 'src/lib'" + (js2-init-scanner) + (should (js2-match-token js2-IMPORT)) + (let ((import (js2-parse-import))) + (should (not (equal nil import))) + (should (= 1 (js2-node-pos import))) + (should (= 16 (js2-node-len import))) + (should (equal nil (js2-import-node-import import))) + (should (equal nil (js2-import-node-from import))))) + +(js2-deftest parse-imported-default-binding "import theDefault from 'src/lib'" + (js2-push-scope (make-js2-scope :pos 0)) + (js2-init-scanner) + (should (js2-match-token js2-IMPORT)) + (let ((import-node (js2-parse-import))) + (should (not (equal nil import-node))) + (should (equal "src/lib" (js2-import-node-module-id import-node))) + (let ((import (js2-import-node-import import-node))) + (should (not (equal nil import))) + (should (equal nil (js2-import-clause-node-namespace-import import))) + (should (equal nil (js2-import-clause-node-named-imports import))) + (let ((default (js2-import-clause-node-default-binding import))) + (should (not (equal nil default))) + (should (js2-export-binding-node-p default)) + (should (equal "theDefault" (js2-name-node-name (js2-export-binding-node-extern-name default))))))) + (should (js2-scope-get-symbol js2-current-scope "theDefault"))) + +(js2-deftest parse-import-namespace-binding "import * as lib from 'src/lib'" + (js2-push-scope (make-js2-scope :pos 0)) + (js2-init-scanner) + (should (js2-match-token js2-IMPORT)) + (let ((import-node (js2-parse-import))) + (should (not (equal nil import-node))) + (should (equal "src/lib" (js2-import-node-module-id import-node))) + (let ((import (js2-import-node-import import-node))) + (should (not (equal nil import))) + (should (equal nil (js2-import-clause-node-default-binding import))) + (should (equal nil (js2-import-clause-node-named-imports import))) + (let ((ns-import (js2-import-clause-node-namespace-import import))) + (should (not (equal nil ns-import))) + (should (js2-namespace-import-node-p ns-import)) + (should (equal "lib" (js2-name-node-name (js2-namespace-import-node-name ns-import))))))) + (should (js2-scope-get-symbol js2-current-scope "lib"))) + +(js2-deftest parse-import-named-imports "import {foo as bar, baz} from 'src/lib'" + (js2-push-scope (make-js2-scope :pos 0)) + (js2-init-scanner) + (should (js2-match-token js2-IMPORT)) + (let ((import-node (js2-parse-import))) + (should (not (equal nil import-node))) + (should (equal "src/lib" (js2-import-node-module-id import-node))) + (let ((import (js2-import-node-import import-node))) + (should (not (equal nil import))) + (should (equal nil (js2-import-clause-node-default-binding import))) + (should (equal nil (js2-import-clause-node-namespace-import import))) + (let ((named-imports (js2-import-clause-node-named-imports import))) + (should (not (equal nil named-imports))) + (should (listp named-imports)) + (should (= 2 (length named-imports))) + (let ((first (nth 0 named-imports)) + (second (nth 1 named-imports))) + (should (equal "bar" (js2-name-node-name (js2-export-binding-node-local-name first)))) + (should (equal "baz" (js2-name-node-name (js2-export-binding-node-local-name second)))))))) + (should (js2-scope-get-symbol js2-current-scope "bar")) + (should (js2-scope-get-symbol js2-current-scope "baz"))) + +(js2-deftest parse-import-default-and-namespace "import stuff, * as lib from 'src/lib'" + (js2-push-scope (make-js2-scope :pos 0)) + (js2-init-scanner) + (should (js2-match-token js2-IMPORT)) + (let ((import-node (js2-parse-import))) + (should (not (equal nil import-node))) + (should (equal "src/lib" (js2-import-node-module-id import-node))) + (let ((import (js2-import-node-import import-node))) + (should (not (equal nil import))) + (should (equal nil (js2-import-clause-node-named-imports import))) + (let ((default (js2-import-clause-node-default-binding import)) + (ns-import (js2-import-clause-node-namespace-import import))) + (should (not (equal nil default))) + (should (equal "stuff" (js2-name-node-name (js2-export-binding-node-local-name default)))) + (should (not (equal nil ns-import))) + (should (js2-namespace-import-node-p ns-import)) + (should (equal "lib" (js2-name-node-name (js2-namespace-import-node-name ns-import))))))) + (should (js2-scope-get-symbol js2-current-scope "stuff")) + (should (js2-scope-get-symbol js2-current-scope "lib"))) + +(js2-deftest parse-import-default-and-named-imports + "import robert as bob, {cookies, pi as PIE} from 'src/lib'" + (js2-push-scope (make-js2-scope :pos 0)) + (js2-init-scanner) + (should (js2-match-token js2-IMPORT)) + (let ((import-node (js2-parse-import))) + (should (not (equal nil import-node))) + (should (equal "src/lib" (js2-import-node-module-id import-node))) + (let ((import (js2-import-node-import import-node))) + (should (not (equal nil import))) + (should (not (equal nil (js2-import-clause-node-named-imports import)))) + (let ((default (js2-import-clause-node-default-binding import)) + (named-imports (js2-import-clause-node-named-imports import))) + (should (not (equal nil default))) + (should (equal "bob" (js2-name-node-name (js2-export-binding-node-local-name default)))) + (should (not (equal nil named-imports))) + (should (= 2 (length named-imports)))))) + (should (js2-scope-get-symbol js2-current-scope "bob")) + (should (js2-scope-get-symbol js2-current-scope "cookies")) + (should (js2-scope-get-symbol js2-current-scope "PIE"))) + +(js2-deftest parse-this-module-in-from-clause "import {url} from this module;" + (js2-push-scope (make-js2-scope :pos 0)) + (js2-init-scanner) + (should (js2-match-token js2-IMPORT)) + (let ((import-node (js2-parse-import))) + (should import-node) + (let ((from-clause (js2-import-node-from import-node))) + (should from-clause) + (should (equal "this" (js2-from-clause-node-module-id from-clause))) + (should (js2-from-clause-node-metadata-p from-clause))))) + +(js2-deftest-parse import-only-for-side-effects "import 'src/lib';") +(js2-deftest-parse import-default-only "import theDefault from 'src/lib';") +(js2-deftest-parse import-named-only "import {one, two} from 'src/lib';") +(js2-deftest-parse import-default-and-named "import theDefault, {one, two} from 'src/lib';") +(js2-deftest-parse import-renaming-default "import * as lib from 'src/mylib';") +(js2-deftest-parse import-renaming-named "import {one as uno, two as dos} from 'src/lib';") +(js2-deftest-parse import-default-and-namespace "import robert as bob, * as lib from 'src/lib';") +(js2-deftest-parse import-from-this-module "import {url} from this module;") + +;; Module Exports + +(js2-deftest export-rexport "export * from 'other/lib'" + (js2-init-scanner) + (should (js2-match-token js2-EXPORT)) + (let ((export-node (js2-parse-export))) + (should export-node) + (should (js2-export-node-from-clause export-node)))) + +(js2-deftest export-export-named-list "export {foo, bar as bang};" + (js2-init-scanner) + (should (js2-match-token js2-EXPORT)) + (let ((export-node (js2-parse-export))) + (should export-node) + (let ((exports (js2-export-node-exports-list export-node))) + (should exports) + (should (= 2 (length exports)))))) + +(js2-deftest re-export-named-list "export {foo, bar as bang} from 'other/lib'" + (js2-init-scanner) + (should (js2-match-token js2-EXPORT)) + (let ((export-node (js2-parse-export))) + (should export-node) + (should (js2-export-node-from-clause export-node)) + (let ((exports (js2-export-node-exports-list export-node))) + (should exports) + (should (= 2 (length exports)))))) + +(js2-deftest export-variable-statement "export var foo = 'bar', baz = 'bang';" + (js2-init-scanner) + (js2-push-scope (make-js2-scope :pos 0)) + (should (js2-match-token js2-EXPORT)) + (let ((export-node (js2-parse-export))) + (should export-node) + (should (js2-export-node-declaration export-node)))) + +(js2-deftest export-const-declaration "export const PI = Math.PI;" + (js2-init-scanner) + (js2-push-scope (make-js2-scope :pos 0)) + (should (js2-match-token js2-EXPORT)) + (let ((export-node (js2-parse-export))) + (should export-node) + (should (js2-export-node-declaration export-node)))) + +(js2-deftest export-let-declaration "export let foo = [1];" + (js2-init-scanner) + (js2-push-scope (make-js2-scope :pos 0)) + (should (js2-match-token js2-EXPORT)) + (let ((export-node (js2-parse-export))) + (should export-node) + (should (js2-var-decl-node-p (js2-export-node-declaration export-node))))) + +(js2-deftest export-class-declaration "export class Foo {};" + (js2-init-scanner) + (js2-push-scope (make-js2-scope :pos 0)) + (should (js2-match-token js2-EXPORT)) + (let ((export-node (js2-parse-export))) + (should export-node) + (should (js2-class-node-p (js2-export-node-declaration export-node))))) + +(js2-deftest export-function-declaration "export default function doStuff() {};" + (js2-init-scanner) + (js2-push-scope (make-js2-scope :pos 0)) + (should (js2-match-token js2-EXPORT)) + (let ((export-node (js2-parse-export))) + (should export-node) + (should (js2-export-node-default export-node)))) + +(js2-deftest export-generator-declaration "export default function* one() {};" + (js2-init-scanner) + (js2-push-scope (make-js2-scope :pos 0)) + (should (js2-match-token js2-EXPORT)) + (let ((export-node (js2-parse-export))) + (should export-node) + (should (js2-export-node-default export-node)))) + +(js2-deftest export-assignment-expression "export default a = b;" + (js2-init-scanner) + (js2-push-scope (make-js2-scope :pos 0)) + (should (js2-match-token js2-EXPORT)) + (let ((export-node (js2-parse-export))) + (should export-node) + (should (js2-export-node-default export-node)))) + +(js2-deftest-parse parse-export-rexport "export * from 'other/lib';") +(js2-deftest-parse parse-export-export-named-list "export {foo, bar as bang};") +(js2-deftest-parse parse-re-export-named-list "export {foo, bar as bang} from 'other/lib';") +(js2-deftest-parse parse-export-const-declaration "export const PI = Math.PI;") +(js2-deftest-parse parse-export-let-declaration "export let foo = [1];") +(js2-deftest-parse parse-export-function-declaration "export default function doStuff() {};") +(js2-deftest-parse parse-export-generator-declaration "export default function* one() {};") +(js2-deftest-parse parse-export-assignment-expression "export default a = b;") + ;;; Strings (js2-deftest-parse string-literal -- 2.39.2