X-Git-Url: https://code.delx.au/gnu-emacs-elpa/blobdiff_plain/6d4aa286dfc8eb8d5ffbdcbfb575391ef829a108..0555a8a3:/js2-mode.el diff --git a/js2-mode.el b/js2-mode.el index 082a5d07d..249f4ca8b 100644 --- a/js2-mode.el +++ b/js2-mode.el @@ -7,7 +7,7 @@ ;; Dmitry Gutov ;; URL: https://github.com/mooz/js2-mode/ ;; http://code.google.com/p/js2-mode/ -;; Version: 20150202 +;; Version: 20150909 ;; Keywords: languages, javascript ;; Package-Requires: ((emacs "24.1") (cl-lib "0.5")) @@ -87,13 +87,16 @@ (require 'cl-lib) (require 'imenu) -(require 'cc-cmds) ; for `c-fill-paragraph' +(require 'js) +(require 'etags) (eval-and-compile - (require 'cc-mode) ; (only) for `c-populate-syntax-table' - (require 'cc-engine)) ; for `c-paragraph-start' et. al. - -(defvar electric-layout-rules) + (if (version< emacs-version "25.0") + (require 'js2-old-indent) + (defvaralias 'js2-basic-offset 'js-indent-level nil) + (defalias 'js2-proper-indentation 'js--proper-indentation) + (defalias 'js2-indent-line 'js-indent-line) + (defalias 'js2-re-search-forward 'js--re-search-forward))) ;;; Externs (variables presumed to be defined by the host system) @@ -188,7 +191,8 @@ Set `js2-include-rhino-externs' to t to include them.") (defvar js2-node-externs (mapcar 'symbol-name '(__dirname __filename Buffer clearInterval clearTimeout require - console exports global module process setInterval setTimeout)) + console exports global module process setInterval setTimeout + querystring)) "Node.js externs. Set `js2-include-node-externs' to t to include them.") @@ -234,78 +238,6 @@ variable with predicate PRED." "An improved JavaScript mode." :group 'languages) -(defcustom js2-basic-offset (if (and (boundp 'c-basic-offset) - (numberp c-basic-offset)) - c-basic-offset - 4) - "Number of spaces to indent nested statements. -Similar to `c-basic-offset'." - :group 'js2-mode - :type 'integer) -(js2-mark-safe-local 'js2-basic-offset 'integerp) - -(defcustom js2-bounce-indent-p nil - "Non-nil to have indent-line function choose among alternatives. -If nil, the indent-line function will indent to a predetermined column -based on heuristic guessing. If non-nil, then if the current line is -already indented to that predetermined column, indenting will choose -another likely column and indent to that spot. Repeated invocation of -the indent-line function will cycle among the computed alternatives. -See the function `js2-bounce-indent' for details. When it is non-nil, -js2-mode also binds `js2-bounce-indent-backwards' to Shift-Tab." - :type 'boolean - :group 'js2-mode) - -(defcustom js2-pretty-multiline-declarations t - "Non-nil to line up multiline declarations vertically: - - var a = 10, - b = 20, - c = 30; - -If the value is t, and the first assigned value in the -declaration is a function/array/object literal spanning several -lines, it won't be indented additionally: - - var o = { var bar = 2, - foo: 3 vs. o = { - }, foo: 3 - bar = 2; }; - -If the value is `all', it will always be indented additionally: - - var o = { - foo: 3 - }; - - var o = { - foo: 3 - }, - bar = 2; - -If the value is `dynamic', it will be indented additionally only -if the declaration contains more than one variable: - - var o = { - foo: 3 - }; - - var o = { - foo: 3 - }, - bar = 2;" - :group 'js2-mode - :type 'symbol) -(js2-mark-safe-local 'js2-pretty-multiline-declarations 'symbolp) - -(defcustom js2-indent-switch-body nil - "When nil, case labels are indented on the same level as the -containing switch statement. Otherwise, all lines inside -switch statement body are indented one additional level." - :type 'boolean - :group 'js2-mode) -(js2-mark-safe-local 'js2-indent-case-same-as-switch 'booleanp) - (defcustom js2-idle-timer-delay 0.2 "Delay in secs before re-parsing after user makes changes. Multiplied by `js2-dynamic-idle-timer-adjust', which see." @@ -735,6 +667,7 @@ List of chars built up while scanning various tokens.") (end -1) (string "") number + number-base regexp-flags comment-type follows-eol-p) @@ -832,6 +765,9 @@ Will only be used when we finish implementing the interpreter.") (js2-deflocal js2-is-in-destructuring nil "True while parsing destructuring expression.") +(js2-deflocal js2-in-use-strict-directive nil + "True while inside a script or function under strict mode.") + (defcustom js2-global-externs nil "A list of any extern names you'd like to consider always declared. This list is global and is used by all `js2-mode' files. @@ -1082,6 +1018,11 @@ in large files.") "Face used to highlight function name in calls." :group 'js2-mode) +(defface js2-object-property + '((t :inherit default)) + "Face used to highlight named property in object literal." + :group 'js2-mode) + (defface js2-instance-member '((t :foreground "DarkOrchid")) "Face used to highlight instance variables in javascript. @@ -1155,6 +1096,11 @@ another file, or you've got a potential bug." :type 'boolean :group 'js2-mode) +(defcustom js2-warn-about-unused-function-arguments nil + "Non-nil to treat function arguments like declared-but-unused variables." + :type 'booleanp + :group 'js2-mode) + (defcustom js2-include-jslint-globals t "Non-nil to include the identifiers from JSLint global declaration (see http://www.jslint.com/lint.html#global) in the @@ -1175,8 +1121,7 @@ information." (define-key map (kbd "C-c C-o") #'js2-mode-toggle-element) (define-key map (kbd "C-c C-w") #'js2-mode-toggle-warnings-and-errors) (define-key map [down-mouse-3] #'js2-down-mouse-3) - (when js2-bounce-indent-p - (define-key map (kbd "") #'js2-indent-bounce-backwards)) + (define-key map [remap js-find-symbol] #'js2-jump-to-definition) (define-key map [menu-bar javascript] (cons "JavaScript" (make-sparse-keymap "JavaScript"))) @@ -1239,6 +1184,24 @@ information." map) "Keymap used in `js2-mode' buffers.") +(defcustom js2-bounce-indent-p nil + "Non-nil to bind `js2-indent-bounce' and `js2-indent-bounce-backward'. +They will augment the default indent-line behavior with cycling +among several computed alternatives. See the function +`js2-bounce-indent' for details. The above commands will be +bound to TAB and backtab." + :type 'boolean + :group 'js2-mode + :set (lambda (sym value) + (set-default sym value) + (let ((map js2-mode-map)) + (if (not value) + (progn + (define-key map "\t" nil) + (define-key map (kbd "") nil)) + (define-key map "\t" #'js2-indent-bounce) + (define-key map (kbd "") #'js2-indent-bounce-backward))))) + (defconst js2-mode-identifier-re "[[:alpha:]_$][[:alnum:]_$]*") (defvar js2-mode-//-comment-re "^\\(\\s-*\\)//.+" @@ -1261,32 +1224,6 @@ First match-group is the leading whitespace.") (js2-deflocal js2-imenu-recorder nil "Private variable") (js2-deflocal js2-imenu-function-map nil "Private variable") -(defvar js2-paragraph-start - "\\(@[[:alpha:]]+\\>\\|$\\)") - -;; Note that we also set a 'c-in-sws text property in html comments, -;; so that `c-forward-sws' and `c-backward-sws' work properly. -(defvar js2-syntactic-ws-start - "\\s \\|/[*/]\\|[\n\r]\\|\\\\[\n\r]\\|\\s!\\|") - -(defvar js2-syntactic-ws-end - "\\s \\|[\n\r/]\\|\\s!") - -(defvar js2-syntactic-eol - (concat "\\s *\\(/\\*[^*\n\r]*" - "\\(\\*+[^*\n\r/][^*\n\r]*\\)*" - "\\*+/\\s *\\)*" - "\\(//\\|/\\*[^*\n\r]*" - "\\(\\*+[^*\n\r/][^*\n\r]*\\)*$" - "\\|\\\\$\\|$\\)") - "Copied from `java-mode'. Needed for some cc-engine functions.") - -(defvar js2-comment-prefix-regexp - "//+\\|\\**") - -(defvar js2-comment-start-skip - "\\(//+\\|/\\*+\\)\\s *") - (defvar js2-mode-verbose-parse-p js2-mode-dev-mode-p "Non-nil to emit status messages during parsing.") @@ -1662,6 +1599,9 @@ the correct number of ARGS must be provided." (js2-msg "msg.no.paren.after.with" "missing ) after with-statement object") +(js2-msg "msg.no.with.strict" + "with statements not allowed in strict mode") + (js2-msg "msg.no.paren.after.let" "missing ( after let") @@ -1777,6 +1717,18 @@ the correct number of ARGS must be provided." (js2-msg "msg.destruct.assign.no.init" "Missing = in destructuring declaration") +(js2-msg "msg.no.octal.strict" + "Octal numbers prohibited in strict mode.") + +(js2-msg "msg.dup.obj.lit.prop.strict" + "Property '%s' already defined in this object literal.") + +(js2-msg "msg.dup.param.strict" + "Parameter '%s' already declared in this function.") + +(js2-msg "msg.bad.id.strict" + "'%s' is not a valid identifier for this use in strict mode.") + ;; ScriptRuntime (js2-msg "msg.no.properties" "%s has no properties.") @@ -1793,6 +1745,12 @@ the correct number of ARGS must be provided." (js2-msg "msg.undeclared.variable" ; added by js2-mode "Undeclared variable or function '%s'") +(js2-msg "msg.unused.variable" ; added by js2-mode + "Unused variable or function '%s'") + +(js2-msg "msg.uninitialized.variable" ; added by js2-mode + "Variable '%s' referenced but never initialized") + (js2-msg "msg.ref.undefined.prop" "Reference to undefined property '%s'") @@ -2173,17 +2131,6 @@ Returns nil if element is not found in the list." (defsubst js2-flag-not-set-p (flags flag) (zerop (logand flags flag))) -(defmacro js2-with-underscore-as-word-syntax (&rest body) - "Evaluate BODY with the _ character set to be word-syntax." - (declare (indent 0) (debug t)) - (let ((old-syntax (make-symbol "old-syntax"))) - `(let ((,old-syntax (string (char-syntax ?_)))) - (unwind-protect - (progn - (modify-syntax-entry ?_ "w" js2-mode-syntax-table) - ,@body) - (modify-syntax-entry ?_ ,old-syntax js2-mode-syntax-table))))) - ;;; AST struct and function definitions ;; flags for ast node property 'member-type (used for e4x operators) @@ -3328,7 +3275,7 @@ The `params' field is a Lisp list of nodes. Each node is either a simple (defun js2-print-function-node (n i) (let* ((pad (js2-make-pad i)) - (getter (js2-node-get-prop n 'GETTER_SETTER)) + (method (js2-node-get-prop n 'METHOD_TYPE)) (name (or (js2-function-node-name n) (js2-function-node-member-expr n))) (params (js2-function-node-params n)) @@ -3336,7 +3283,7 @@ The `params' field is a Lisp list of nodes. Each node is either a simple (rest-p (js2-function-node-rest-p n)) (body (js2-function-node-body n)) (expr (not (eq (js2-function-node-form n) 'FUNCTION_STATEMENT)))) - (unless (or getter arrow) + (unless (or method arrow) (insert pad "function") (when (eq (js2-function-node-generator-type n) 'STAR) (insert "*"))) @@ -3742,10 +3689,13 @@ Returns 0 if NODE is nil or its identifier field is nil." (js2-current-token-beg))) (value (js2-current-token-string)) (num-value (js2-token-number - (js2-current-token)))))) + (js2-current-token))) + (num-base (js2-token-number-base + (js2-current-token)))))) "AST node for a number literal." value ; the original string, e.g. "6.02e23" - num-value) ; the parsed number value + num-value ; the parsed number value + num-base) ; the number's base (put 'cl-struct-js2-number-node 'js2-visitor 'js2-visit-none) (put 'cl-struct-js2-number-node 'js2-printer 'js2-print-number-node) @@ -3934,6 +3884,30 @@ optional `js2-expr-node'" (js2-print-list (js2-object-node-elems n)) (insert "}")) +(cl-defstruct (js2-computed-prop-name-node + (:include js2-node) + (:constructor nil) + (:constructor make-js2-computed-prop-name-node + (&key + (type js2-LB) + expr + (pos (js2-current-token-beg)) + (len (- js2-ts-cursor + (js2-current-token-beg)))))) + "AST node for a `ComputedPropertyName'." + expr) + +(put 'cl-struct-js2-computed-prop-name-node 'js2-visitor 'js2-visit-computed-prop-name-node) +(put 'cl-struct-js2-computed-prop-name-node 'js2-printer 'js2-print-computed-prop-name-node) + +(defun js2-visit-computed-prop-name-node (n v) + (js2-visit-ast (js2-computed-prop-name-node-expr n) v)) + +(defun js2-print-computed-prop-name-node (n i) + (insert (js2-make-pad i) "[") + (js2-print-ast (js2-computed-prop-name-node-expr n) 0) + (insert "]")) + (cl-defstruct (js2-object-prop-node (:include js2-infix-node) (:constructor nil) @@ -3942,63 +3916,49 @@ optional `js2-expr-node'" len left right op-pos))) "AST node for an object literal prop:value entry. -The `left' field is the property: a name node, string node or number node. -The `right' field is a `js2-node' representing the initializer value. -If the property is abbreviated, the node's `SHORTHAND' property is non-nil -and both fields have the same value.") +The `left' field is the property: a name node, string node, +number node or expression node. The `right' field is a +`js2-node' representing the initializer value. If the property +is abbreviated, the node's `SHORTHAND' property is non-nil and +both fields have the same value.") (put 'cl-struct-js2-object-prop-node 'js2-visitor 'js2-visit-infix-node) (put 'cl-struct-js2-object-prop-node 'js2-printer 'js2-print-object-prop-node) (defun js2-print-object-prop-node (n i) (let* ((left (js2-object-prop-node-left n)) - (right (js2-object-prop-node-right n)) - (computed (not (or (js2-string-node-p left) - (js2-number-node-p left) - (js2-name-node-p left))))) - (insert (js2-make-pad i)) - (if computed - (insert "[")) - (js2-print-ast left 0) - (if computed - (insert "]")) + (right (js2-object-prop-node-right n))) + (js2-print-ast left i) (if (not (js2-node-get-prop n 'SHORTHAND)) (progn (insert ": ") (js2-print-ast right 0))))) -(cl-defstruct (js2-getter-setter-node +(cl-defstruct (js2-method-node (:include js2-infix-node) (:constructor nil) - (:constructor make-js2-getter-setter-node (&key type ; GET, SET, or FUNCTION - (pos js2-ts-cursor) - len left right))) - "AST node for a getter/setter property in an object literal. -The `left' field is the `js2-name-node' naming the getter/setter prop. + (:constructor make-js2-method-node (&key type ; GET, SET, or FUNCTION + (pos js2-ts-cursor) + len left right))) + "AST node for a method in an object literal or a class body. +The `left' field is the `js2-name-node' naming the method. The `right' field is always an anonymous `js2-function-node' with a node -property `GETTER_SETTER' set to js2-GET, js2-SET, or js2-FUNCTION. ") +property `METHOD_TYPE' set to js2-GET, js2-SET, or js2-FUNCTION. ") -(put 'cl-struct-js2-getter-setter-node 'js2-visitor 'js2-visit-infix-node) -(put 'cl-struct-js2-getter-setter-node 'js2-printer 'js2-print-getter-setter) +(put 'cl-struct-js2-method-node 'js2-visitor 'js2-visit-infix-node) +(put 'cl-struct-js2-method-node 'js2-printer 'js2-print-method) -(defun js2-print-getter-setter (n i) +(defun js2-print-method (n i) (let* ((pad (js2-make-pad i)) - (left (js2-getter-setter-node-left n)) - (right (js2-getter-setter-node-right n)) - (computed (not (or (js2-string-node-p left) - (js2-number-node-p left) - (js2-name-node-p left))))) + (left (js2-method-node-left n)) + (right (js2-method-node-right n))) (insert pad) (if (/= (js2-node-type n) js2-FUNCTION) (insert (if (= (js2-node-type n) js2-GET) "get " "set "))) (when (and (js2-function-node-p right) (eq 'STAR (js2-function-node-generator-type right))) (insert "*")) - (when computed - (insert "[")) (js2-print-ast left 0) - (when computed - (insert "]")) (js2-print-ast right 0))) (cl-defstruct (js2-prop-get-node @@ -6089,8 +6049,8 @@ its relevant fields and puts it into `js2-ti-tokens'." while (js2-digit-p c)))) (js2-unget-char) (let ((str (js2-set-string-from-buffer token))) - (setf (js2-token-number token) - (js2-string-to-number str base))) + (setf (js2-token-number token) (js2-string-to-number str base) + (js2-token-number-base token) base)) (throw 'return js2-NUMBER)) ;; is it a string? (when (or (memq c '(?\" ?\')) @@ -6807,6 +6767,8 @@ Shown at or above `js2-highlight-level' 3.") (prop (if (string-match js2-ecma-object-props prop-name) 'font-lock-constant-face)))))) + (when (and (not face) target (not call-p) prop-name) + (setq face 'js2-object-property)) (when face (let ((pos (+ (js2-node-pos parent) ; absolute (js2-node-pos prop)))) ; relative @@ -6875,11 +6837,11 @@ of a simple name. Called before EXPR has a parent node." (defconst js2-jsdoc-param-tag-regexp (concat "^\\s-*\\*+\\s-*\\(@" - "\\(?:param\\|argument\\)" + "\\(?:param\\|arg\\(?:ument\\)?\\|prop\\(?:erty\\)?\\)" "\\)" "\\s-*\\({[^}]+}\\)?" ; optional type "\\s-*\\[?\\([[:alnum:]_$\.]+\\)?\\]?" ; name - "\\>") + "\\_>") "Matches jsdoc tags with optional type and optional param name.") (defconst js2-jsdoc-typed-tag-regexp @@ -6917,7 +6879,6 @@ of a simple name. Called before EXPR has a parent node." "memberOf" "name" "namespace" - "property" "since" "suppress" "this" @@ -7081,6 +7042,160 @@ it is considered declared." (js2-report-warning "msg.undeclared.variable" name pos (- end pos) 'js2-external-variable)))))) +(defun js2--add-or-update-symbol (symbol inition used vars) + "Add or update SYMBOL entry in VARS, an hash table. +SYMBOL is a js2-name-node, INITION either nil, t, or ?P, +respectively meaning that SYMBOL is a mere declaration, an +assignment or a function parameter; when USED is t, the symbol +node is assumed to be an usage and thus added to the list stored +in the cdr of the entry. +" + (let* ((nm (js2-name-node-name symbol)) + (es (js2-node-get-enclosing-scope symbol)) + (ds (js2-get-defining-scope es nm))) + (when (and ds (not (equal nm "arguments"))) + (let* ((sym (js2-scope-get-symbol ds nm)) + (var (gethash sym vars)) + (err-var-p (js2-catch-node-p ds))) + (unless inition + (setq inition err-var-p)) + (if var + (progn + (when (and inition (not (equal (car var) ?P))) + (setcar var inition)) + (when used + (push symbol (cdr var)))) + ;; do not consider the declaration of catch parameter as an usage + (when (and err-var-p used) + (setq used nil)) + (puthash sym (cons inition (if used (list symbol))) vars)))))) + +(defun js2--classify-variables () + "Collect and classify variables declared or used within js2-mode-ast. +Traverse the whole ast tree returning a summary of the variables +usage as an hash-table, keyed by their corresponding symbol table +entry. +Each variable is described by a tuple where the car is a flag +indicating whether the variable has been initialized and the cdr +is a possibly empty list of name nodes where it is used. External +symbols, i.e. those not present in the whole scopes hierarchy, +are ignored." + (let ((vars (make-hash-table :test #'eq :size 100))) + (js2-visit-ast + js2-mode-ast + (lambda (node end-p) + (when (null end-p) + (cond + ((js2-var-init-node-p node) + ;; take note about possibly initialized declarations + (let ((target (js2-var-init-node-target node)) + (initializer (js2-var-init-node-initializer node))) + (when target + (let* ((parent (js2-node-parent node)) + (grandparent (if parent (js2-node-parent parent))) + (inited (not (null initializer)))) + (unless inited + (setq inited + (and grandparent + (js2-for-in-node-p grandparent) + (memq target + (mapcar #'js2-var-init-node-target + (js2-var-decl-node-kids + (js2-for-in-node-iterator grandparent))))))) + (js2--add-or-update-symbol target inited nil vars))))) + + ((js2-assign-node-p node) + ;; take note about assignments + (let ((left (js2-assign-node-left node))) + (when (js2-name-node-p left) + (js2--add-or-update-symbol left t nil vars)))) + + ((js2-prop-get-node-p node) + ;; handle x.y.z nodes, considering only x + (let ((left (js2-prop-get-node-left node))) + (when (js2-name-node-p left) + (js2--add-or-update-symbol left nil t vars)))) + + ((js2-name-node-p node) + ;; take note about used variables + (let ((parent (js2-node-parent node))) + (when parent + (unless (or (and (js2-var-init-node-p parent) ; handled above + (eq node (js2-var-init-node-target parent))) + (and (js2-assign-node-p parent) + (eq node (js2-assign-node-left parent))) + (js2-prop-get-node-p parent)) + (let ((used t) inited) + (cond + ((and (js2-function-node-p parent) + (js2-wrapper-function-p parent)) + (setq inited (if (memq node (js2-function-node-params parent)) ?P t))) + + ((js2-for-in-node-p parent) + (if (eq node (js2-for-in-node-iterator parent)) + (setq inited t used nil))) + + ((js2-function-node-p parent) + (setq inited (if (memq node (js2-function-node-params parent)) ?P t) + used nil))) + + (unless used + (let ((grandparent (js2-node-parent parent))) + (when grandparent + (setq used (js2-return-node-p grandparent))))) + + (js2--add-or-update-symbol node inited used vars)))))))) + t)) + vars)) + +(defun js2--get-name-node (node) + (cond + ((js2-name-node-p node) node) + ((js2-function-node-p node) + (js2-function-node-name node)) + ((js2-class-node-p node) + (js2-class-node-name node)) + ((js2-comp-loop-node-p node) + (js2-comp-loop-node-iterator node)) + (t node))) + +(defun js2--highlight-unused-variable (symbol info) + (let ((name (js2-symbol-name symbol)) + (inited (car info)) + (refs (cdr info)) + pos len) + (unless (and inited refs) + (if refs + (dolist (ref refs) + (setq pos (js2-node-abs-pos ref)) + (setq len (js2-name-node-len ref)) + (js2-report-warning "msg.uninitialized.variable" name pos len + 'js2-warning)) + (when (or js2-warn-about-unused-function-arguments + (not (eq inited ?P))) + (let* ((symn (js2-symbol-ast-node symbol)) + (namen (js2--get-name-node symn))) + (unless (js2-node-top-level-decl-p namen) + (setq pos (js2-node-abs-pos namen)) + (setq len (js2-name-node-len namen)) + (js2-report-warning "msg.unused.variable" name pos len + 'js2-warning)))))))) + +(defun js2-highlight-unused-variables () + "Highlight unused variables." + (let ((vars (js2--classify-variables))) + (maphash #'js2--highlight-unused-variable vars))) + +;;;###autoload +(define-minor-mode js2-highlight-unused-variables-mode + "Toggle highlight of unused variables." + :lighter "" + (if js2-highlight-unused-variables-mode + (add-hook 'js2-post-parse-callbacks + #'js2-highlight-unused-variables nil t) + (remove-hook 'js2-post-parse-callbacks + #'js2-highlight-unused-variables t))) + (defun js2-set-default-externs () "Set the value of `js2-default-externs' based on the various `js2-include-?-externs' variables." @@ -7696,6 +7811,15 @@ Returns t on match, nil if no match." (defsubst js2-exit-switch () (pop js2-loop-and-switch-set)) +(defsubst js2-get-directive (node) + "Return NODE's value if it is a directive, nil otherwise. + +A directive is an otherwise-meaningless expression statement +consisting of a string literal, such as \"use strict\"." + (and (js2-expr-stmt-node-p node) + (js2-string-node-p (setq node (js2-expr-stmt-node-expr node))) + (js2-string-node-value node))) + (defun js2-parse (&optional buf cb) "Tell the js2 parser to parse a region of JavaScript. @@ -7757,14 +7881,18 @@ leaving a statement, an expression, or a function definition." Scanner should be initialized." (let ((pos js2-ts-cursor) (end js2-ts-cursor) ; in case file is empty - root n tt) + root n tt + (in-directive-prologue t) + (js2-in-use-strict-directive js2-in-use-strict-directive) + directive) ;; initialize buffer-local parsing vars (setf root (make-js2-ast-root :buffer (buffer-name) :pos pos) js2-current-script-or-fn root js2-current-scope root js2-nesting-of-function 0 js2-labeled-stmt nil - js2-recorded-identifiers nil) ; for js2-highlight + js2-recorded-identifiers nil ; for js2-highlight + js2-in-use-strict-directive nil) (while (/= (setq tt (js2-get-token)) js2-EOF) (if (= tt js2-FUNCTION) (progn @@ -7773,7 +7901,14 @@ Scanner should be initialized." (js2-parse-function-stmt)))) ;; not a function - parse a statement (js2-unget-token) - (setq n (js2-parse-statement))) + (setq n (js2-parse-statement)) + (when in-directive-prologue + (setq directive (js2-get-directive n)) + (cond + ((null directive) + (setq in-directive-prologue nil)) + ((string= directive "use strict") + (setq js2-in-use-strict-directive t))))) ;; add function or statement to script (setq end (js2-node-end n)) (js2-block-node-push root n)) @@ -7811,16 +7946,34 @@ Scanner should be initialized." (let ((pos (js2-current-token-beg)) ; LC position (pn (make-js2-block-node)) ; starts at LC position tt - end) + end + not-in-directive-prologue + node + directive) (cl-incf js2-nesting-of-function) (unwind-protect (while (not (or (= (setq tt (js2-peek-token)) js2-ERROR) (= tt js2-EOF) (= tt js2-RC))) - (js2-block-node-push pn (if (/= tt js2-FUNCTION) - (js2-parse-statement) - (js2-get-token) - (js2-parse-function-stmt)))) + (js2-block-node-push + pn + (if (/= tt js2-FUNCTION) + (if not-in-directive-prologue + (js2-parse-statement) + (setq node (js2-parse-statement) + directive (js2-get-directive node)) + (cond + ((null directive) + (setq not-in-directive-prologue t)) + ((string= directive "use strict") + ;; Back up and reparse the function, because new rules apply + ;; to the function name and parameters. + (when (not js2-in-use-strict-directive) + (setq js2-in-use-strict-directive t) + (throw 'reparse t)))) + node) + (js2-get-token) + (js2-parse-function-stmt)))) (cl-decf js2-nesting-of-function)) (setq end (js2-current-token-end)) ; assume no curly and leave at current token (if (js2-must-match js2-RC "msg.no.brace.after.body" pos) @@ -7833,35 +7986,78 @@ Scanner should be initialized." (defun js2-define-destruct-symbols (node decl-type face &optional ignore-not-in-block) "Declare and fontify destructuring parameters inside NODE. -NODE is either `js2-array-node', `js2-object-node', or `js2-name-node'." - (cond - ((js2-name-node-p node) - (let (leftpos) - (js2-define-symbol decl-type (js2-name-node-name node) - node ignore-not-in-block) - (when face - (js2-set-face (setq leftpos (js2-node-abs-pos node)) - (+ leftpos (js2-node-len node)) - face 'record)))) - ((js2-object-node-p node) - (dolist (elem (js2-object-node-elems node)) - (js2-define-destruct-symbols - ;; In abbreviated destructuring {a, b}, right == left. - (js2-object-prop-node-right elem) - decl-type face ignore-not-in-block))) - ((js2-array-node-p node) - (dolist (elem (js2-array-node-elems node)) - (when elem - (js2-define-destruct-symbols elem decl-type face ignore-not-in-block)))) - (t (js2-report-error "msg.no.parm" nil (js2-node-abs-pos node) - (js2-node-len node))))) +NODE is either `js2-array-node', `js2-object-node', or `js2-name-node'. + +Return a list of `js2-name-node' nodes representing the symbols +declared; probably to check them for errors." + (let (name-nodes) + (cond + ((js2-name-node-p node) + (let (leftpos) + (js2-define-symbol decl-type (js2-name-node-name node) + node ignore-not-in-block) + (when face + (js2-set-face (setq leftpos (js2-node-abs-pos node)) + (+ leftpos (js2-node-len node)) + face 'record)) + (list node))) + ((js2-object-node-p node) + (dolist (elem (js2-object-node-elems node)) + ;; js2-infix-node-p catches both object prop node and initialized + ;; binding element (which is directly an infix node). + (when (js2-infix-node-p elem) + (push (js2-define-destruct-symbols + (js2-infix-node-left elem) + decl-type face ignore-not-in-block) + name-nodes))) + (apply #'append (nreverse name-nodes))) + ((js2-array-node-p node) + (dolist (elem (js2-array-node-elems node)) + (when elem + (if (js2-infix-node-p elem) (setq elem (js2-infix-node-left elem))) + (push (js2-define-destruct-symbols + elem decl-type face ignore-not-in-block) + name-nodes))) + (apply #'append (nreverse name-nodes))) + (t (js2-report-error "msg.no.parm" nil (js2-node-abs-pos node) + (js2-node-len node)) + nil)))) + +(defvar js2-illegal-strict-identifiers + '("eval" "arguments") + "Identifiers not allowed as variables in strict mode.") + +(defun js2-check-strict-identifier (name-node) + "Check that NAME-NODE makes a legal strict mode identifier." + (when js2-in-use-strict-directive + (let ((param-name (js2-name-node-name name-node))) + (when (member param-name js2-illegal-strict-identifiers) + (js2-report-error "msg.bad.id.strict" param-name + (js2-node-abs-pos name-node) (js2-node-len name-node)))))) + +(defun js2-check-strict-function-params (preceding-params params) + "Given PRECEDING-PARAMS in a function's parameter list, check +for strict mode errors caused by PARAMS." + (when js2-in-use-strict-directive + (dolist (param params) + (let ((param-name (js2-name-node-name param))) + (js2-check-strict-identifier param) + (when (cl-some (lambda (param) + (string= (js2-name-node-name param) param-name)) + preceding-params) + (js2-report-error "msg.dup.param.strict" param-name + (js2-node-abs-pos param) (js2-node-len param))))))) (defun js2-parse-function-params (function-type fn-node pos) + "Parse the parameters of a function of FUNCTION-TYPE +represented by FN-NODE at POS." (if (js2-match-token js2-RP) (setf (js2-function-node-rp fn-node) (- (js2-current-token-beg) pos)) (let ((paren-free-arrow (and (eq function-type 'FUNCTION_ARROW) (eq (js2-current-token-type) js2-NAME))) - params param default-found rest-param-at) + params param + param-name-nodes new-param-name-nodes + rest-param-at) (when paren-free-arrow (js2-unget-token)) (cl-loop for tt = (js2-peek-token) @@ -7871,12 +8067,11 @@ NODE is either `js2-array-node', `js2-object-node', or `js2-name-node'." ((and (not paren-free-arrow) (or (= tt js2-LB) (= tt js2-LC))) (js2-get-token) - (when default-found - (js2-report-error "msg.no.default.after.default.param")) - (setq param (js2-parse-destruct-primary-expr)) - (js2-define-destruct-symbols param - js2-LP - 'js2-function-param) + (setq param (js2-parse-destruct-primary-expr) + new-param-name-nodes (js2-define-destruct-symbols + param js2-LP 'js2-function-param)) + (js2-check-strict-function-params param-name-nodes new-param-name-nodes) + (setq param-name-nodes (append param-name-nodes new-param-name-nodes)) (push param params)) ;; variable name (t @@ -7890,15 +8085,11 @@ NODE is either `js2-array-node', `js2-object-node', or `js2-name-node'." (js2-record-face 'js2-function-param) (setq param (js2-create-name-node)) (js2-define-symbol js2-LP (js2-current-token-string) param) + (js2-check-strict-function-params param-name-nodes (list param)) + (setq param-name-nodes (append param-name-nodes (list param))) ;; default parameter value - (when (or (and default-found - (not rest-param-at) - (js2-must-match js2-ASSIGN - "msg.no.default.after.default.param" - (js2-node-pos param) - (js2-node-len param))) - (and (>= js2-language-version 200) - (js2-match-token js2-ASSIGN))) + (when (and (>= js2-language-version 200) + (js2-match-token js2-ASSIGN)) (cl-assert (not paren-free-arrow)) (let* ((pos (js2-node-pos param)) (tt (js2-current-token-type)) @@ -7908,8 +8099,7 @@ NODE is either `js2-array-node', `js2-object-node', or `js2-name-node'." (len (- (js2-node-end right) pos))) (setq param (make-js2-assign-node :type tt :pos pos :len len :op-pos op-pos - :left left :right right) - default-found t) + :left left :right right)) (js2-node-add-children param left right))) (push param params))) (when (and rest-param-at (> (length params) (1+ rest-param-at))) @@ -7973,10 +8163,7 @@ Last token scanned is the close-curly for the function body." (js2-must-match js2-LP "msg.no.paren.parms") (js2-parse-function 'FUNCTION_EXPRESSION pos star-p name))) -(defun js2-parse-function (function-type pos star-p &optional name) - "Function parser. FUNCTION-TYPE is a symbol, POS is the -beginning of the first token (function keyword, unless it's an -arrow function), NAME is js2-name-node." +(defun js2-parse-function-internal (function-type pos star-p &optional name) (let (fn-node lp) (if (= (js2-current-token-type) js2-LP) ; eventually matched LP? (setq lp (js2-current-token-beg))) @@ -7991,7 +8178,9 @@ arrow function), NAME is js2-name-node." (when (and (eq function-type 'FUNCTION_STATEMENT) (cl-plusp (js2-name-node-length name))) ;; Function statements define a symbol in the enclosing scope - (js2-define-symbol js2-FUNCTION (js2-name-node-name name) fn-node))) + (js2-define-symbol js2-FUNCTION (js2-name-node-name name) fn-node)) + (when js2-in-use-strict-directive + (js2-check-strict-identifier name))) (if (or (js2-inside-function) (cl-plusp js2-nesting-of-with)) ;; 1. Nested functions are not affected by the dynamic scope flag ;; as dynamic scope is already a parent of their scope. @@ -8037,6 +8226,29 @@ arrow function), NAME is js2-name-node." (setf (js2-scope-parent-scope fn-node) js2-current-scope) fn-node)) +(defun js2-parse-function (function-type pos star-p &optional name) + "Function parser. FUNCTION-TYPE is a symbol, POS is the +beginning of the first token (function keyword, unless it's an +arrow function), NAME is js2-name-node." + (let ((continue t) + ts-state + fn-node + ;; Preserve strict state outside this function. + (js2-in-use-strict-directive js2-in-use-strict-directive)) + ;; Parse multiple times if a new strict mode directive is discovered in the + ;; function body, as new rules will be retroactively applied to the legality + ;; of function names and parameters. + (while continue + (setq ts-state (make-js2-ts-state)) + (setq continue (catch 'reparse + (setq fn-node (js2-parse-function-internal + function-type pos star-p name)) + ;; Don't continue. + nil)) + (when continue + (js2-ts-seek ts-state))) + fn-node)) + (defun js2-parse-statements (&optional parent) "Parse a statement list. Last token consumed must be js2-LC. @@ -8724,7 +8936,8 @@ Last matched token must be js2-FOR." (t (js2-must-match-name "msg.bad.catchcond") (setq param (js2-create-name-node)) - (js2-define-symbol js2-LET (js2-current-token-string) param)))) + (js2-define-symbol js2-LET (js2-current-token-string) param) + (js2-check-strict-identifier param)))) ;; Catch condition. (if (js2-match-token js2-IF) (setq guard-kwd (- (js2-current-token-beg) catch-pos) @@ -8854,6 +9067,8 @@ does not match an existing label, reports an error and returns nil." (defun js2-parse-with () "Parser for with-statement. Last matched token must be js2-WITH." + (when js2-in-use-strict-directive + (js2-report-error "msg.no.with.strict")) (let ((pos (js2-current-token-beg)) obj body pn lp rp) (if (js2-must-match js2-LP "msg.no.paren.with") @@ -9170,7 +9385,8 @@ Returns the parsed `js2-var-decl-node' expression node." nbeg (js2-current-token-beg) nend (js2-current-token-end) end nend) - (js2-define-symbol decl-type (js2-current-token-string) name js2-in-for-init))) + (js2-define-symbol decl-type (js2-current-token-string) name js2-in-for-init) + (js2-check-strict-identifier name))) (when (js2-match-token js2-ASSIGN) (setq init (js2-parse-assign-expr) end (js2-node-end init)) @@ -9251,8 +9467,12 @@ If NODE is non-nil, it is the AST node associated with the symbol." (len (if node (js2-node-len node)))) (cond ((and symbol ; already defined - (or (= sdt js2-CONST) ; old version is const - (= decl-type js2-CONST) ; new version is const + (or (if js2-in-use-strict-directive + ;; two const-bound vars in this block have same name + (and (= sdt js2-CONST) + (eq defining-scope js2-current-scope)) + (or (= sdt js2-CONST) ; old version is const + (= decl-type js2-CONST))) ; new version is const ;; two let-bound vars in this block have same name (and (= sdt js2-LET) (eq defining-scope js2-current-scope)))) @@ -9264,15 +9484,21 @@ If NODE is non-nil, it is the AST node associated with the symbol." ((= sdt js2-FUNCTION) "msg.function.redecl") (t "msg.parm.redecl")) name pos len)) - ((= decl-type js2-LET) - (if (and (not ignore-not-in-block) + ((or (= decl-type js2-LET) + ;; strict mode const is scoped to the current LexicalEnvironment + (and js2-in-use-strict-directive + (= decl-type js2-CONST))) + (if (and (= decl-type js2-LET) + (not ignore-not-in-block) (or (= (js2-node-type js2-current-scope) js2-IF) (js2-loop-node-p js2-current-scope))) (js2-report-error "msg.let.decl.not.in.block") (js2-define-new-symbol decl-type name node))) ((or (= decl-type js2-VAR) - (= decl-type js2-CONST) - (= decl-type js2-FUNCTION)) + (= decl-type js2-FUNCTION) + ;; sloppy mode const is scoped to the current VariableEnvironment + (and (not js2-in-use-strict-directive) + (= decl-type js2-CONST))) (if symbol (if (and js2-strict-var-redeclaration-warning (= sdt js2-VAR)) (js2-add-strict-warning "msg.var.redecl" name) @@ -9357,6 +9583,10 @@ If NODE is non-nil, it is the AST node associated with the symbol." ;; tt express assignment (=, |=, ^=, ..., %=) (setq op-pos (- (js2-current-token-beg) pos) ; relative left pn) + ;; The assigned node could be a js2-prop-get-node (foo.bar = 0), we only + ;; care about assignment to strict variable names. + (when (js2-name-node-p left) + (js2-check-strict-identifier left)) (setq right (js2-parse-assign-expr) pn (make-js2-assign-node :type tt :pos pos @@ -9404,7 +9634,7 @@ If NODE is non-nil, it is the AST node associated with the symbol." (js2-node-add-children pn test-expr if-true if-false)) pn)) -(defun js2-make-binary (type left parser) +(defun js2-make-binary (type left parser &optional no-get) "Helper for constructing a binary-operator AST node. LEFT is the left-side-expression, already parsed, and the binary operator should have just been matched. @@ -9415,7 +9645,7 @@ FIXME: The latter option is unused?" (op-pos (- (js2-current-token-beg) pos)) (right (if (js2-node-p parser) parser - (js2-get-token) + (unless no-get (js2-get-token)) (funcall parser))) (pn (make-js2-infix-node :type type :pos pos @@ -9756,9 +9986,9 @@ Returns an expression tree that includes PN, the parent node." (setq pn (js2-parse-tagged-template pn (make-js2-string-node :type tt)))) (t (js2-unget-token) - (setq continue nil)))) - (if (>= js2-highlight-level 2) - (js2-parse-highlight-member-expr-node pn)) + (setq continue nil))) + (if (>= js2-highlight-level 2) + (js2-parse-highlight-member-expr-node pn))) pn)) (defun js2-parse-tagged-template (tag-node tpl-node) @@ -9986,7 +10216,7 @@ For instance, @[expr], @*::[expr], or ns::[expr]." "Parse a literal (leaf) expression of some sort. Includes complex literals such as functions, object-literals, array-literals, array comprehensions and regular expressions." - (let (tt) + (let (tt node) (setq tt (js2-current-token-type)) (cond ((= tt js2-CLASS) @@ -10007,7 +10237,11 @@ array-literals, array comprehensions and regular expressions." ((= tt js2-NAME) (js2-parse-name tt)) ((= tt js2-NUMBER) - (make-js2-number-node)) + (setq node (make-js2-number-node)) + (when (and js2-in-use-strict-directive + (= (js2-number-node-num-base node) 8)) + (js2-report-error "msg.no.octal.strict")) + node) ((or (= tt js2-STRING) (= tt js2-NO_SUBS_TEMPLATE)) (make-js2-string-node :type tt)) ((= tt js2-TEMPLATE_HEAD) @@ -10132,14 +10366,20 @@ array-literals, array comprehensions and regular expressions." (apply #'js2-node-add-children pn (js2-array-node-elems pn))) ;; destructuring binding (js2-is-in-destructuring - (push (if (or (= tt js2-LC) - (= tt js2-LB) - (= tt js2-NAME)) - ;; [a, b, c] | {a, b, c} | {a:x, b:y, c:z} | a - (js2-parse-destruct-primary-expr) - ;; invalid pattern + (push (cond + ((and (= tt js2-NAME) + (= js2-ASSIGN (js2-peek-token))) + ;; a=defaultValue + (js2-parse-initialized-binding (js2-parse-name js2-NAME))) + ((or (= tt js2-LC) + (= tt js2-LB) + (= tt js2-NAME)) + ;; [a, b, c] | {a, b, c} | {a:x, b:y, c:z} | a + (js2-parse-destruct-primary-expr)) + ;; invalid pattern + (t (js2-report-error "msg.bad.var") - (make-js2-error-node)) + (make-js2-error-node))) elems) (setq after-lb-or-comma nil after-comma nil)) @@ -10224,7 +10464,7 @@ We should have just parsed the 'for' keyword before calling this function." pn)) (defun js2-parse-comprehension (pos form) - (let (loops filters expr result) + (let (loops filters expr result last) (unwind-protect (progn (js2-unget-token) @@ -10235,7 +10475,8 @@ We should have just parsed the 'for' keyword before calling this function." (js2-parse-comp-loop loop))) (while (js2-match-token js2-IF) (push (car (js2-parse-condition)) filters)) - (setq expr (js2-parse-assign-expr))) + (setq expr (js2-parse-assign-expr)) + (setq last (car loops))) (dolist (_ loops) (js2-pop-scope))) (setq result (make-js2-comp-node :pos pos @@ -10246,6 +10487,7 @@ We should have just parsed the 'for' keyword before calling this function." :form form)) (apply #'js2-node-add-children result (js2-comp-node-loops result)) (apply #'js2-node-add-children result expr (js2-comp-node-filters result)) + (setf (js2-scope-parent-scope result) last) result)) (defun js2-parse-comp-loop (pn &optional only-of-p) @@ -10350,11 +10592,29 @@ If ONLY-OF-P is non-nil, only the 'for (foo of bar)' form is allowed." (apply #'js2-node-add-children result (js2-object-node-elems result)) result)) +(defun js2-property-key-string (property-node) + "Return the key of PROPERTY-NODE (a `js2-object-prop-node' or +`js2-method-node') as a string, or nil if it can't be +represented as a string (e.g., the key is computed by an +expression)." + (let ((key (js2-infix-node-left property-node))) + (when (js2-computed-prop-name-node-p key) + (setq key (js2-computed-prop-name-node-expr key))) + (cond + ((js2-name-node-p key) + (js2-name-node-name key)) + ((js2-string-node-p key) + (js2-string-node-value key)) + ((js2-number-node-p key) + (js2-number-node-value key))))) + (defun js2-parse-object-literal-elems (&optional class-p) (let ((pos (js2-current-token-beg)) (static nil) (continue t) - tt elems elem after-comma previous-token) + tt elems elem + elem-key-string previous-elem-key-string + after-comma previous-token) (while continue (setq tt (js2-get-prop-name-token) static nil @@ -10397,6 +10657,10 @@ If ONLY-OF-P is non-nil, only the 'for (foo of bar)' form is allowed." (if after-comma (js2-parse-warn-trailing-comma "msg.extra.trailing.comma" pos elems after-comma))) + ;; Skip semicolons in a class body + ((and class-p + (= tt js2-SEMI)) + nil) (t (js2-report-error "msg.bad.prop") (unless js2-recover-from-parse-errors @@ -10413,33 +10677,34 @@ If ONLY-OF-P is non-nil, only the 'for (foo of bar)' form is allowed." (setq after-comma (js2-current-token-end))) (js2-unget-token) (unless class-p (setq continue nil)))) - ;; Append any parsed element. - (if elem (push elem elems))) ; end loop + (when elem + (when (and js2-in-use-strict-directive + (setq elem-key-string (js2-property-key-string elem)) + (cl-some + (lambda (previous-elem) + (and (setq previous-elem-key-string + (js2-property-key-string previous-elem)) + ;; Check if the property is a duplicate. + (string= previous-elem-key-string elem-key-string) + ;; But make an exception for getter / setter pairs. + (not (and (js2-method-node-p elem) + (js2-method-node-p previous-elem) + (/= (js2-method-node-type elem) + (js2-method-node-type previous-elem)))))) + elems)) + (js2-report-error "msg.dup.obj.lit.prop.strict" + elem-key-string + (js2-node-abs-pos (js2-infix-node-left elem)) + (js2-node-len (js2-infix-node-left elem)))) + ;; Append any parsed element. + (push elem elems))) ; end loop (js2-must-match js2-RC "msg.no.brace.prop") (nreverse elems))) (defun js2-parse-named-prop (tt pos previous-token) "Parse a name, string, or getter/setter object property. When `js2-is-in-destructuring' is t, forms like {a, b, c} will be permitted." - (let ((key (cond - ;; Literal string keys: {'foo': 'bar'} - ((= tt js2-STRING) - (make-js2-string-node)) - ;; Handle computed keys: {[Symbol.iterator]: ...}, *[1+2]() {...}}, - ;; {[foo + bar]() { ... }}, {[get ['x' + 1]() {...}} - ((and (= tt js2-LB) - (>= js2-language-version 200)) - (prog1 (js2-parse-expr) - (js2-must-match js2-RB "msg.missing.computed.rb"))) - ;; Numeric keys: {12: 'foo'}, {10.7: 'bar'} - ((= tt js2-NUMBER) - (make-js2-number-node)) - ;; Unquoted names: {foo: 12} - ((= tt js2-NAME) - (js2-create-name-node)) - ;; Anything else is an error - (t (js2-report-error "msg.bad.prop")))) - expr + (let ((key (js2-parse-prop-name tt)) (prop (and previous-token (js2-token-string previous-token))) (property-type (when previous-token (if (= (js2-token-type previous-token) js2-MUL) @@ -10456,26 +10721,61 @@ When `js2-is-in-destructuring' is t, forms like {a, b, c} will be permitted." (>= js2-language-version 200)) (when (js2-name-node-p key) ; highlight function name properties (js2-record-face 'font-lock-function-name-face)) - (js2-parse-getter-setter-prop pos key property-type)) + (js2-parse-method-prop pos key property-type)) + ;; binding element with initializer + ((and (= (js2-peek-token) js2-ASSIGN) + (>= js2-language-version 200)) + (js2-parse-initialized-binding key)) ;; regular prop (t - (prog1 - (setq expr (js2-parse-plain-property key)) + (let ((beg (js2-current-token-beg)) + (end (js2-current-token-end)) + (expr (js2-parse-plain-property key))) (when (and (= tt js2-NAME) (not js2-is-in-destructuring) js2-highlight-external-variables (js2-node-get-prop expr 'SHORTHAND)) (js2-record-name-node key)) - (js2-set-face (js2-current-token-beg) (js2-current-token-end) + (js2-set-face beg end (if (js2-function-node-p (js2-object-prop-node-right expr)) 'font-lock-function-name-face - 'font-lock-variable-name-face) - 'record)))))) + 'js2-object-property) + 'record) + expr))))) + +(defun js2-parse-initialized-binding (name) + "Parse a `SingleNameBinding' with initializer. + +`name' is the `BindingIdentifier'." + (when (js2-match-token js2-ASSIGN) + (js2-make-binary js2-ASSIGN name 'js2-parse-assign-expr t))) + +(defun js2-parse-prop-name (tt) + (cond + ;; Literal string keys: {'foo': 'bar'} + ((= tt js2-STRING) + (make-js2-string-node)) + ;; Handle computed keys: {[Symbol.iterator]: ...}, *[1+2]() {...}}, + ;; {[foo + bar]() { ... }}, {[get ['x' + 1]() {...}} + ((and (= tt js2-LB) + (>= js2-language-version 200)) + (make-js2-computed-prop-name-node + :expr (prog1 (js2-parse-assign-expr) + (js2-must-match js2-RB "msg.missing.computed.rb")))) + ;; Numeric keys: {12: 'foo'}, {10.7: 'bar'} + ((= tt js2-NUMBER) + (make-js2-number-node)) + ;; Unquoted names: {foo: 12} + ((= tt js2-NAME) + (js2-create-name-node)) + ;; Anything else is an error + (t (js2-report-error "msg.bad.prop")))) (defun js2-parse-plain-property (prop) "Parse a non-getter/setter property in an object literal. -PROP is the node representing the property: a number, name or string." +PROP is the node representing the property: a number, name, +string or expression." (let* ((tt (js2-get-token)) (pos (js2-node-pos prop)) colon expr result) @@ -10513,11 +10813,11 @@ PROP is the node representing the property: a number, name or string." (js2-node-add-children result prop expr) result)))) -(defun js2-parse-getter-setter-prop (pos prop type-string) - "Parse getter or setter property in an object literal. +(defun js2-parse-method-prop (pos prop type-string) + "Parse method property in an object literal or a class body. JavaScript syntax is: - { get foo() {...}, set foo(x) {...} } + { foo(...) {...}, get foo() {...}, set foo(x) {...}, *foo(...) {...} } and expression closure style is also supported @@ -10537,15 +10837,15 @@ TYPE-STRING is a string `get', `set', `*', or nil, indicating a found keyword." (js2-report-error "msg.bad.prop") (if (cl-plusp (length (js2-function-name fn))) (js2-report-error "msg.bad.prop"))) - (js2-node-set-prop fn 'GETTER_SETTER type) ; for codegen + (js2-node-set-prop fn 'METHOD_TYPE type) ; for codegen (when (string= type-string "*") (setf (js2-function-node-generator-type fn) 'STAR)) (setq end (js2-node-end fn) - result (make-js2-getter-setter-node :type type - :pos pos - :len (- end pos) - :left prop - :right fn)) + result (make-js2-method-node :type type + :pos pos + :len (- end pos) + :left prop + :right fn)) (js2-node-add-children result prop fn) result)) @@ -10566,22 +10866,19 @@ And, if CHECK-ACTIVATION-P is non-nil, use the value of TOKEN." (js2-check-activation-name s (or token js2-NAME))) name)) -;;; Indentation support +;;; Indentation support (bouncing) -;; This indenter is based on Karl Landström's "javascript.el" indenter. -;; Karl cleverly deduces that the desired indentation level is often a -;; function of paren/bracket/brace nesting depth, which can be determined -;; quickly via the built-in `parse-partial-sexp' function. His indenter -;; then does some equally clever checks to see if we're in the context of a -;; substatement of a possibly braceless statement keyword such as if, while, -;; or finally. This approach yields pretty good results. +;; In recent-enough Emacs, we reuse the indentation code from +;; `js-mode'. To continue support for the older versions, some code +;; that was here previously was moved to `js2-old-indent.el'. -;; The indenter is often "wrong", however, and needs to be overridden. -;; The right long-term solution is probably to emulate (or integrate -;; with) cc-engine, but it's a nontrivial amount of coding. Even when a -;; parse tree from `js2-parse' is present, which is not true at the -;; moment the user is typing, computing indentation is still thousands -;; of lines of code to handle every possible syntactic edge case. +;; Whichever indenter is used, it's often "wrong", however, and needs +;; to be overridden. The right long-term solution is probably to +;; emulate (or integrate with) cc-engine, but it's a nontrivial amount +;; of coding. Even when a parse tree from `js2-parse' is present, +;; which is not true at the moment the user is typing, computing +;; indentation is still thousands of lines of code to handle every +;; possible syntactic edge case. ;; In the meantime, the compromise solution is that we offer a "bounce ;; indenter", configured with `js2-bounce-indent-p', which cycles the @@ -10590,359 +10887,6 @@ And, if CHECK-ACTIVATION-P is non-nil, use the value of TOKEN." ;; move the line towards its desired indentation when manually ;; overriding Karl's heuristic nesting guesser. -;; I've made miscellaneous tweaks to Karl's code to handle some Ecma -;; extensions such as `let' and Array comprehensions. Major kudos to -;; Karl for coming up with the initial approach, which packs a lot of -;; punch for so little code. - -(defconst js2-possibly-braceless-keywords-re - (concat "else[ \t]+if\\|for[ \t]+each\\|" - (regexp-opt '("catch" "do" "else" "finally" "for" "if" - "try" "while" "with" "let"))) - "Regular expression matching keywords that are optionally -followed by an opening brace.") - -(defconst js2-indent-operator-re - (concat "[-+*/%<>&^|?:.]\\([^-+*/]\\|$\\)\\|!?=\\|" - (regexp-opt '("in" "instanceof") 'words)) - "Regular expression matching operators that affect indentation -of continued expressions.") - -(defconst js2-declaration-keyword-re - (regexp-opt '("var" "let" "const") 'words) - "Regular expression matching variable declaration keywords.") - -(defun js2-re-search-forward-inner (regexp &optional bound count) - "Auxiliary function for `js2-re-search-forward'." - (let (parse saved-point) - (while (> count 0) - (re-search-forward regexp bound) - (setq parse (if saved-point - (parse-partial-sexp saved-point (point)) - (syntax-ppss (point)))) - (cond ((nth 3 parse) - (re-search-forward - (concat "\\(\\=\\|[^\\]\\|^\\)" (string (nth 3 parse))) - (save-excursion (end-of-line) (point)) t)) - ((nth 7 parse) - (forward-line)) - ((or (nth 4 parse) - (and (eq (char-before) ?\/) (eq (char-after) ?\*))) - (re-search-forward "\\*/")) - (t - (setq count (1- count)))) - (setq saved-point (point)))) - (point)) - -(defun js2-re-search-forward (regexp &optional bound noerror count) - "Search forward but ignore strings and comments. -Invokes `re-search-forward' but treats the buffer as if strings -and comments have been removed." - (let ((saved-point (point))) - (condition-case err - (cond ((null count) - (js2-re-search-forward-inner regexp bound 1)) - ((< count 0) - (js2-re-search-backward-inner regexp bound (- count))) - ((> count 0) - (js2-re-search-forward-inner regexp bound count))) - (search-failed - (goto-char saved-point) - (unless noerror - (error (error-message-string err))))))) - -(defun js2-re-search-backward-inner (regexp &optional bound count) - "Auxiliary function for `js2-re-search-backward'." - (let (parse) - (while (> count 0) - (re-search-backward regexp bound) - (setq parse (syntax-ppss (point))) - (cond ((nth 3 parse) - (re-search-backward - (concat "\\([^\\]\\|^\\)" (string (nth 3 parse))) - (line-beginning-position) t)) - ((nth 7 parse) - (goto-char (nth 8 parse))) - ((or (nth 4 parse) - (and (eq (char-before) ?/) (eq (char-after) ?*))) - (re-search-backward "/\\*")) - (t - (setq count (1- count)))))) - (point)) - -(defun js2-re-search-backward (regexp &optional bound noerror count) - "Search backward but ignore strings and comments. -Invokes `re-search-backward' but treats the buffer as if strings -and comments have been removed." - (let ((saved-point (point))) - (condition-case err - (cond ((null count) - (js2-re-search-backward-inner regexp bound 1)) - ((< count 0) - (js2-re-search-forward-inner regexp bound (- count))) - ((> count 0) - (js2-re-search-backward-inner regexp bound count))) - (search-failed - (goto-char saved-point) - (unless noerror - (error (error-message-string err))))))) - -(defun js2-looking-at-operator-p () - "Return non-nil if text after point is a non-comma operator." - (and (looking-at js2-indent-operator-re) - (or (not (looking-at ":")) - (save-excursion - (and (js2-re-search-backward "[?:{]\\|\\" nil t) - (looking-at "?")))))) - -(defun js2-continued-expression-p () - "Return non-nil if the current line continues an expression." - (save-excursion - (back-to-indentation) - (or (js2-looking-at-operator-p) - (when (catch 'found - (while (and (re-search-backward "\n" nil t) - (let ((state (syntax-ppss))) - (when (nth 4 state) - (goto-char (nth 8 state))) ;; skip comments - (skip-chars-backward " \t") - (if (bolp) - t - (throw 'found t)))))) - (backward-char) - (when (js2-looking-at-operator-p) - (backward-char) - (not (looking-at "\\*\\|\\+\\+\\|--\\|/[/*]"))))))) - -(defun js2-end-of-do-while-loop-p () - "Return non-nil if word after point is `while' of a do-while -statement, else returns nil. A braceless do-while statement -spanning several lines requires that the start of the loop is -indented to the same column as the current line." - (interactive) - (save-excursion - (when (looking-at "\\s-*\\") - (if (save-excursion - (skip-chars-backward "[ \t\n]*}") - (looking-at "[ \t\n]*}")) - (save-excursion - (backward-list) (backward-word 1) (looking-at "\\")) - (js2-re-search-backward "\\" (point-at-bol) t) - (or (looking-at "\\") - (let ((saved-indent (current-indentation))) - (while (and (js2-re-search-backward "^[ \t]*\\<" nil t) - (/= (current-indentation) saved-indent))) - (and (looking-at "[ \t]*\\") - (not (js2-re-search-forward - "\\" (point-at-eol) t)) - (= (current-indentation) saved-indent)))))))) - -(defun js2-multiline-decl-indentation () - "Return the declaration indentation column if the current line belongs -to a multiline declaration statement. See `js2-pretty-multiline-declarations'." - (let (forward-sexp-function ; use Lisp version - at-opening-bracket) - (save-excursion - (back-to-indentation) - (when (not (looking-at js2-declaration-keyword-re)) - (when (looking-at js2-indent-operator-re) - (goto-char (match-end 0))) ; continued expressions are ok - (while (and (not at-opening-bracket) - (not (bobp)) - (let ((pos (point))) - (save-excursion - (js2-backward-sws) - (or (eq (char-before) ?,) - (and (not (eq (char-before) ?\;)) - (prog2 (skip-syntax-backward ".") - (looking-at js2-indent-operator-re) - (js2-backward-sws)) - (not (eq (char-before) ?\;))) - (js2-same-line pos))))) - (condition-case _ - (backward-sexp) - (scan-error (setq at-opening-bracket t)))) - (when (looking-at js2-declaration-keyword-re) - (goto-char (match-end 0)) - (1+ (current-column))))))) - -(defun js2-ctrl-statement-indentation () - "Return the proper indentation of current line if it is a control statement. -Returns an indentation if this line starts the body of a control -statement without braces, else returns nil." - (let (forward-sexp-function) - (save-excursion - (back-to-indentation) - (when (and (not (js2-same-line (point-min))) - (not (looking-at "{")) - (js2-re-search-backward "[[:graph:]]" nil t) - (not (looking-at "[{([]")) - (progn - (forward-char) - (when (= (char-before) ?\)) - ;; scan-sexps sometimes throws an error - (ignore-errors (backward-sexp)) - (skip-chars-backward " \t" (point-at-bol))) - (let ((pt (point))) - (back-to-indentation) - (when (looking-at "}[ \t]*") - (goto-char (match-end 0))) - (and (looking-at js2-possibly-braceless-keywords-re) - (= (match-end 0) pt) - (not (js2-end-of-do-while-loop-p)))))) - (+ (current-indentation) js2-basic-offset))))) - -(defun js2-indent-in-array-comp (parse-status) - "Return non-nil if we think we're in an array comprehension. -In particular, return the buffer position of the first `for' kwd." - (let ((bracket (nth 1 parse-status)) - (end (point))) - (when bracket - (save-excursion - (goto-char bracket) - (when (looking-at "\\[") - (forward-char 1) - (js2-forward-sws) - (if (looking-at "[[{]") - (let (forward-sexp-function) ; use Lisp version - (forward-sexp) ; skip destructuring form - (js2-forward-sws) - (if (and (/= (char-after) ?,) ; regular array - (looking-at "for")) - (match-beginning 0))) - ;; to skip arbitrary expressions we need the parser, - ;; so we'll just guess at it. - (if (and (> end (point)) ; not empty literal - (re-search-forward "[^,]]* \\(for\\) " end t) - ;; not inside comment or string literal - (let ((state (parse-partial-sexp bracket (point)))) - (not (or (nth 3 state) (nth 4 state))))) - (match-beginning 1)))))))) - -(defun js2-array-comp-indentation (parse-status for-kwd) - (if (js2-same-line for-kwd) - ;; first continuation line - (save-excursion - (goto-char (nth 1 parse-status)) - (forward-char 1) - (skip-chars-forward " \t") - (current-column)) - (save-excursion - (goto-char for-kwd) - (current-column)))) - -(defun js2-maybe-goto-declaration-keyword-end (bracket) - "Helper function for `js2-proper-indentation'. -Depending on the value of `js2-pretty-multiline-declarations', -move point to the end of a variable declaration keyword so that -indentation is aligned to that column." - (cond - ((eq js2-pretty-multiline-declarations 'all) - (when (looking-at js2-declaration-keyword-re) - (goto-char (1+ (match-end 0))))) - ((eq js2-pretty-multiline-declarations 'dynamic) - (let (declaration-keyword-end - at-closing-bracket-p - comma-p) - (when (looking-at js2-declaration-keyword-re) - ;; Preserve the match data lest it somehow be overridden. - (setq declaration-keyword-end (match-end 0)) - (save-excursion - (goto-char bracket) - (setq at-closing-bracket-p - ;; Handle scan errors gracefully. - (condition-case nil - (progn - ;; Use the regular `forward-sexp-function' because the - ;; normal one for this mode uses the AST. - (let (forward-sexp-function) - (forward-sexp)) - t) - (error nil))) - (when at-closing-bracket-p - (js2-forward-sws) - (setq comma-p (looking-at-p ",")))) - (when comma-p - (goto-char (1+ declaration-keyword-end)))))))) - -(defun js2-proper-indentation (parse-status) - "Return the proper indentation for the current line." - (save-excursion - (back-to-indentation) - (let* ((ctrl-stmt-indent (js2-ctrl-statement-indentation)) - (at-closing-bracket (looking-at "[]})]")) - (same-indent-p (or at-closing-bracket - (looking-at "\\[^:]") - (and (looking-at "\\= js2-language-version 170) - (not (js2-same-line bracket)) - (setq beg (js2-indent-in-array-comp parse-status)) - (>= (point) (save-excursion - (goto-char beg) - (point-at-bol)))) ; at or after first loop? - (js2-array-comp-indentation parse-status beg)) - - (ctrl-stmt-indent) - - ((and declaration-indent continued-expr-p) - (+ declaration-indent js2-basic-offset)) - - (declaration-indent) - - (bracket - (goto-char bracket) - (cond - ((looking-at "[({[][ \t]*\\(/[/*]\\|$\\)") - (when (save-excursion (skip-chars-backward " \t)") - (looking-at ")")) - (backward-list)) - (back-to-indentation) - (js2-maybe-goto-declaration-keyword-end bracket) - (setq indent - (cond (same-indent-p - (current-column)) - (continued-expr-p - (+ (current-column) (* 2 js2-basic-offset))) - (t - (+ (current-column) js2-basic-offset)))) - (if (and js2-indent-switch-body - (not at-closing-bracket) - (looking-at "\\_")) - (+ indent js2-basic-offset) - indent)) - (t - (unless same-indent-p - (forward-char) - (skip-chars-forward " \t")) - (current-column)))) - - (continued-expr-p js2-basic-offset) - - (t 0))))) - -(defun js2-lineup-comment (parse-status) - "Indent a multi-line block comment continuation line." - (let* ((beg (nth 8 parse-status)) - (first-line (js2-same-line beg)) - (offset (save-excursion - (goto-char beg) - (if (looking-at "/\\*") - (+ 1 (current-column)) - 0)))) - (unless first-line - (indent-line-to offset)))) - (defun js2-backward-sws () "Move backward through whitespace and comments." (interactive) @@ -10953,15 +10897,6 @@ indentation is aligned to that column." (interactive) (while (forward-comment 1))) -(defun js2-current-indent (&optional pos) - "Return column of indentation on current line. -If POS is non-nil, go to that point and return indentation for that line." - (save-excursion - (if pos - (goto-char pos)) - (back-to-indentation) - (current-column))) - (defun js2-arglist-close () "Return non-nil if we're on a line beginning with a close-paren/brace." (save-excursion @@ -11009,14 +10944,14 @@ If POS is non-nil, go to that point and return indentation for that line." (skip-chars-forward " \t") (looking-at "case\\s-.+:"))) -(defun js2-bounce-indent (normal-col parse-status &optional backwards) +(defun js2-bounce-indent (normal-col parse-status &optional backward) "Cycle among alternate computed indentation positions. PARSE-STATUS is the result of `parse-partial-sexp' from the beginning of the buffer to the current point. NORMAL-COL is the indentation column computed by the heuristic guesser based on current paren, bracket, brace and statement nesting. If BACKWARDS, cycle positions in reverse." - (let ((cur-indent (js2-current-indent)) + (let ((cur-indent (current-indentation)) (old-buffer-undo-list buffer-undo-list) ;; Emacs 21 only has `count-lines', not `line-number-at-pos' (current-line (save-excursion @@ -11119,8 +11054,8 @@ in reverse." (js2-indent-objlit-arg-p parse-status)) (setq main-pos basic-offset)) - ;; if bouncing backwards, reverse positions list - (if backwards + ;; if bouncing backward, reverse positions list + (if backward (setq positions (reverse positions))) ;; record whether we're already sitting on one of the alternatives @@ -11164,12 +11099,6 @@ in reverse." ;; see commentary for `js2-mode-last-indented-line' (setq js2-mode-last-indented-line current-line)))) -(defun js2-indent-bounce-backwards () - "Calls `js2-indent-line'. When `js2-bounce-indent-p', -cycles between the computed indentation positions in reverse order." - (interactive) - (js2-indent-line t)) - (defun js2-1-line-comment-continuation-p () "Return t if we're in a 1-line comment continuation. If so, we don't ever want to use bounce-indent." @@ -11185,8 +11114,8 @@ If so, we don't ever want to use bounce-indent." (forward-line 0)) (looking-at "\\s-*//"))))) -(defun js2-indent-line (&optional bounce-backwards) - "Indent the current line as JavaScript source text." +(defun js2-indent-bounce (&optional backward) + "Indent the current line, bouncing between several positions." (interactive) (let (parse-status offset indent-col ;; Don't whine about errors/warnings when we're indenting. @@ -11199,22 +11128,22 @@ If so, we don't ever want to use bounce-indent." (point)))) ;; Don't touch multiline strings. (unless (nth 3 parse-status) - (js2-with-underscore-as-word-syntax - (if (nth 4 parse-status) - (js2-lineup-comment parse-status) - (setq indent-col (js2-proper-indentation parse-status)) - ;; See comments below about `js2-mode-last-indented-line'. - (cond - ;; bounce-indenting is disabled during electric-key indent. - ;; It doesn't work well on first line of buffer. - ((and js2-bounce-indent-p - (not (js2-same-line (point-min))) - (not (js2-1-line-comment-continuation-p))) - (js2-bounce-indent indent-col parse-status bounce-backwards)) - ;; just indent to the guesser's likely spot - (t (indent-line-to indent-col)))) - (when (cl-plusp offset) - (forward-char offset)))))) + (setq indent-col (js2-proper-indentation parse-status)) + (cond + ;; It doesn't work well on first line of buffer. + ((and (not (nth 4 parse-status)) + (not (js2-same-line (point-min))) + (not (js2-1-line-comment-continuation-p))) + (js2-bounce-indent indent-col parse-status backward)) + ;; just indent to the guesser's likely spot + (t (indent-line-to indent-col))) + (when (cl-plusp offset) + (forward-char offset))))) + +(defun js2-indent-bounce-backward () + "Indent the current line, bouncing between positions in reverse." + (interactive) + (js2-indent-bounce t)) (defun js2-indent-region (start end) "Indent the region, but don't use bounce indenting." @@ -11240,9 +11169,11 @@ such as `js-mode', while retaining the asynchronous error/warning highlighting features of `js2-mode'." :group 'js2-mode :lighter " js-lint" - (if js2-minor-mode - (js2-minor-mode-enter) - (js2-minor-mode-exit))) + (if (derived-mode-p 'js2-mode) + (setq js2-minor-mode nil) + (if js2-minor-mode + (js2-minor-mode-enter) + (js2-minor-mode-exit)))) (defun js2-minor-mode-enter () "Initialization for `js2-minor-mode'." @@ -11387,18 +11318,13 @@ Selecting an error will jump it to the corresponding source-buffer error. (message msg)))))) ;;;###autoload -(define-derived-mode js2-mode prog-mode "Javascript-IDE" - ;; FIXME: Should derive from js-mode. +(define-derived-mode js2-mode js-mode "Javascript-IDE" "Major mode for editing JavaScript code." - ;; Used by comment-region; don't change it. - (set (make-local-variable 'comment-start) "//") - (set (make-local-variable 'comment-end) "") - (set (make-local-variable 'comment-start-skip) js2-comment-start-skip) (set (make-local-variable 'max-lisp-eval-depth) (max max-lisp-eval-depth 3000)) (set (make-local-variable 'indent-line-function) #'js2-indent-line) (set (make-local-variable 'indent-region-function) #'js2-indent-region) - (set (make-local-variable 'fill-paragraph-function) #'c-fill-paragraph) + (set (make-local-variable 'syntax-propertize-function) nil) (set (make-local-variable 'comment-line-break-function) #'js2-line-break) (set (make-local-variable 'beginning-of-defun-function) #'js2-beginning-of-defun) (set (make-local-variable 'end-of-defun-function) #'js2-end-of-defun) @@ -11410,30 +11336,6 @@ Selecting an error will jump it to the corresponding source-buffer error. ;; needed for M-x rgrep, among other things (put 'js2-mode 'find-tag-default-function #'js2-mode-find-tag) - (set (make-local-variable 'electric-indent-chars) - (append "{}()[]:;,*." electric-indent-chars)) - (set (make-local-variable 'electric-layout-rules) - '((?\; . after) (?\{ . after) (?\} . before))) - - ;; some variables needed by cc-engine for paragraph-fill, etc. - (setq c-comment-prefix-regexp js2-comment-prefix-regexp - c-comment-start-regexp "/[*/]\\|\\s|" - c-line-comment-starter "//" - c-paragraph-start js2-paragraph-start - c-paragraph-separate "$" - c-syntactic-ws-start js2-syntactic-ws-start - c-syntactic-ws-end js2-syntactic-ws-end - c-syntactic-eol js2-syntactic-eol) - - (let ((c-buffer-is-cc-mode t)) - ;; Copied from `js-mode'. Also see Bug#6071. - (make-local-variable 'paragraph-start) - (make-local-variable 'paragraph-separate) - (make-local-variable 'paragraph-ignore-fill-prefix) - (make-local-variable 'adaptive-fill-mode) - (make-local-variable 'adaptive-fill-regexp) - (c-setup-paragraph-variables)) - (setq font-lock-defaults '(nil t)) ;; Experiment: make reparse-delay longer for longer files. @@ -12325,17 +12227,15 @@ destroying the region selection." "Replacement for `find-tag-default'. `find-tag-default' returns a ridiculous answer inside comments." (let (beg end) - (js2-with-underscore-as-word-syntax - (save-excursion - (if (and (not (looking-at "[[:alnum:]_$]")) - (looking-back "[[:alnum:]_$]")) - (setq beg (progn (forward-word -1) (point)) - end (progn (forward-word 1) (point))) - (setq beg (progn (forward-word 1) (point)) - end (progn (forward-word -1) (point)))) - (replace-regexp-in-string - "[\"']" "" - (buffer-substring-no-properties beg end)))))) + (save-excursion + (if (looking-at "\\_>") + (setq beg (progn (forward-symbol -1) (point)) + end (progn (forward-symbol 1) (point))) + (setq beg (progn (forward-symbol 1) (point)) + end (progn (forward-symbol -1) (point)))) + (replace-regexp-in-string + "[\"']" "" + (buffer-substring-no-properties beg end))))) (defun js2-mode-forward-sibling () "Move to the end of the sibling following point in parent. @@ -12450,6 +12350,129 @@ it marks the next defun after the ones already marked." (unless (js2-ast-root-p fn) (narrow-to-region beg (+ beg (js2-node-len fn)))))) +(defun js2-jump-to-definition (&optional arg) + "Jump to the definition of an object's property, variable or function." + (interactive "P") + (ring-insert find-tag-marker-ring (point-marker)) + (let* ((node (js2-node-at-point)) + (parent (js2-node-parent node)) + (names (if (js2-prop-get-node-p parent) + (reverse (let ((temp (js2-compute-nested-prop-get parent))) + (cl-loop for n in temp + with result = '() + do (push n result) + until (equal node n) + finally return result))))) + node-init) + (unless (and (js2-name-node-p node) + (not (js2-var-init-node-p parent)) + (not (js2-function-node-p parent))) + (error "Node is not a supported jump node")) + (push (or (and names (pop names)) + (unless (and (js2-object-prop-node-p parent) + (eq node (js2-object-prop-node-left parent))) + node)) names) + (setq node-init (js2-search-scope node names)) + + ;; todo: display list of results in buffer + ;; todo: group found references by buffer + (unless node-init + (switch-to-buffer + (catch 'found + (unless arg + (mapc (lambda (b) + (with-current-buffer b + (when (derived-mode-p 'js2-mode) + (setq node-init (js2-search-scope js2-mode-ast names)) + (if node-init + (throw 'found b))))) + (buffer-list))) + nil))) + (setq node-init (if (listp node-init) (car node-init) node-init)) + (unless node-init + (pop-tag-mark) + (error "No jump location found")) + (goto-char (js2-node-abs-pos node-init)))) + +(defun js2-search-object (node name-node) + "Check if object NODE contains element with NAME-NODE." + (cl-assert (js2-object-node-p node)) + ;; Only support name-node and nodes for the time being + (cl-loop for elem in (js2-object-node-elems node) + for left = (js2-object-prop-node-left elem) + if (or (and (js2-name-node-p left) + (equal (js2-name-node-name name-node) + (js2-name-node-name left))) + (and (js2-string-node-p left) + (string= (js2-name-node-name name-node) + (js2-string-node-value left)))) + return elem)) + +(defun js2-search-object-for-prop (object prop-names) + "Return node in OBJECT that matches PROP-NAMES or nil. +PROP-NAMES is a list of values representing a path to a value in OBJECT. +i.e. ('name' 'value') = {name : { value: 3}}" + (let (node + (temp-object object) + (temp t) ;temporay node + (names prop-names)) + (while (and temp names (js2-object-node-p temp-object)) + (setq temp (js2-search-object temp-object (pop names))) + (and (setq node temp) + (setq temp-object (js2-object-prop-node-right temp)))) + (unless names node))) + +(defun js2-search-scope (node names) + "Searches NODE scope for jump location matching NAMES. +NAMES is a list of property values to search for. For functions +and variables NAMES will contain one element." + (let (node-init + (val (js2-name-node-name (car names)))) + (setq node-init (js2-get-symbol-declaration node val)) + + (when (> (length names) 1) + + ;; Check var declarations + (when (and node-init (string= val (js2-name-node-name node-init))) + (let ((parent (js2-node-parent node-init)) + (temp-names names)) + (pop temp-names) ;; First element is var name + (setq node-init (when (js2-var-init-node-p parent) + (js2-search-object-for-prop + (js2-var-init-node-initializer parent) + temp-names))))) + + ;; Check all assign nodes + (js2-visit-ast + js2-mode-ast + (lambda (node endp) + (unless endp + (if (js2-assign-node-p node) + (let ((left (js2-assign-node-left node)) + (right (js2-assign-node-right node)) + (temp-names names)) + (when (js2-prop-get-node-p left) + (let* ((prop-list (js2-compute-nested-prop-get left)) + (found (cl-loop for prop in prop-list + until (not (string= (js2-name-node-name + (pop temp-names)) + (js2-name-node-name prop))) + if (not temp-names) return prop)) + (found-node (or found + (when (js2-object-node-p right) + (js2-search-object-for-prop right + temp-names))))) + (if found-node (push found-node node-init)))))) + t)))) + node-init)) + +(defun js2-get-symbol-declaration (node name) + "Find scope for NAME from NODE." + (let ((scope (js2-get-defining-scope + (or (js2-node-get-enclosing-scope node) + node) name))) + (if scope (js2-symbol-ast-node (js2-scope-get-symbol scope name))))) + (provide 'js2-mode) ;;; js2-mode.el ends here