diff options
author | Jackson Ray Hamilton <jackson@jacksonrayhamilton.com> | 2019-02-11 03:00:34 -0800 |
---|---|---|
committer | Jackson Ray Hamilton <jackson@jacksonrayhamilton.com> | 2019-04-08 22:48:21 -0700 |
commit | be86ece42cbb6204480c794d018b02fbda74689b (patch) | |
tree | a4734ce3c80cfc605851c11c320f544135c4d968 /lisp/progmodes | |
parent | 27e9bce77db54464737aa5be1ce7142b55f25952 (diff) | |
download | emacs-be86ece42cbb6204480c794d018b02fbda74689b.tar.gz |
js-syntax-propertize: Disambiguate JS from JSX, fixing some indents
Fix some JSX indentation bugs:
- Bug#24896 / https://github.com/mooz/js2-mode/issues/389
- Bug#30225
- https://github.com/mooz/js2-mode/issues/459
* lisp/progmodes/js.el (js--dotted-captured-name-re)
(js--unary-keyword-re, js--unary-keyword-p)
(js--disambiguate-beginning-of-jsx-tag)
(js--disambiguate-end-of-jsx-tag)
(js--disambiguate-js-from-jsx): New variables and functions.
(js-syntax-propertize): Additionally clarify when syntax is JS so that
‘(with-syntax-table sgml-mode-syntax-table …)’ does not mistake some
JS punctuation syntax for SGML parenthesis syntax, namely ‘<’ and ‘>’.
* test/manual/indent/js-jsx-unclosed-2.js: Add additional test for
unary operator parsing.
Diffstat (limited to 'lisp/progmodes')
-rw-r--r-- | lisp/progmodes/js.el | 100 |
1 files changed, 99 insertions, 1 deletions
diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el index 5b992535a8c..d0556f3538e 100644 --- a/lisp/progmodes/js.el +++ b/lisp/progmodes/js.el @@ -82,6 +82,10 @@ (concat js--name-re "\\(?:\\." js--name-re "\\)*") "Regexp matching a dot-separated sequence of JavaScript names.") +(defconst js--dotted-captured-name-re + (concat "\\(" js--name-re "\\)\\(?:\\." js--name-re "\\)*") + "Like `js--dotted-name-re', but capture the first name.") + (defconst js--cpp-name-re js--name-re "Regexp matching a C preprocessor name.") @@ -1731,6 +1735,99 @@ This performs fontification according to `js--class-styles'." 'syntax-table (string-to-syntax "\"/")) (goto-char end))))) +(defconst js--unary-keyword-re + (js--regexp-opt-symbol '("await" "delete" "typeof" "void" "yield")) + "Regexp matching unary operator keywords.") + +(defun js--unary-keyword-p (string) + "Check if STRING is a unary operator keyword in JavaScript." + (string-match-p js--unary-keyword-re string)) + +(defun js--disambiguate-beginning-of-jsx-tag () + "Parse enough to determine if a JSX tag starts here. +Disambiguate JSX from equality operators by testing for syntax +only valid as JSX." + ;; “</…” - a JSXClosingElement. + ;; “<>” - a JSXOpeningFragment. + (if (memq (char-after) '(?\/ ?\>)) t + (save-excursion + (skip-chars-forward " \t\n") + (and + (looking-at js--dotted-captured-name-re) + ;; Don’t match code like “if (i < await foo)” + (not (js--unary-keyword-p (match-string 1))) + (progn + (goto-char (match-end 0)) + (skip-chars-forward " \t\n") + (or + ;; “>”, “/>” - tag enders. + ;; “{” - a JSXExpressionContainer. + (memq (char-after) '(?\> ?\/ ?\{)) + ;; Check if a JSXAttribute follows. + (looking-at js--name-start-re))))))) + +(defun js--disambiguate-end-of-jsx-tag () + "Parse enough to determine if a JSX tag ends here. +Disambiguate JSX from equality operators by testing for syntax +only valid as JSX, or extremely unlikely except as JSX." + (save-excursion + (backward-char) + ;; “…/>” - a self-closing JSXOpeningElement. + ;; “</>” - a JSXClosingFragment. + (if (= (char-before) ?/) t + (let (last-tag-or-attr-name last-non-unary-p) + (catch 'match + (while t + (skip-chars-backward " \t\n") + ;; Check if the end of a JSXAttribute value or + ;; JSXExpressionContainer almost certainly precedes. + ;; The only valid JS this misses is + ;; - {} > foo + ;; - "bar" > foo + ;; which is no great loss, IMHO… + (if (memq (char-before) '(?\} ?\" ?\' ?\`)) (throw 'match t) + (if (and last-tag-or-attr-name last-non-unary-p + ;; “<”, “</” - tag starters. + (memq (char-before) '(?\< ?\/))) + ;; Leftmost name parsed was the name of a + ;; JSXOpeningElement. + (throw 'match t)) + ;; Technically the dotted name could span multiple + ;; lines, but dear God WHY?! Also, match greedily to + ;; ensure the entire name is valid. + (if (looking-back js--dotted-captured-name-re (point-at-bol) t) + (if (and (setq last-non-unary-p (not (js--unary-keyword-p (match-string 1)))) + last-tag-or-attr-name) + ;; Valid (non-unary) name followed rightwards by + ;; another name (any will do, including + ;; keywords) is invalid JS, but valid JSX. + (throw 'match t) + ;; Remember match and skip backwards over it when + ;; it is the first matched name or the N+1th + ;; matched unary name (unary names on the left are + ;; still ambiguously JS or JSX, so keep parsing to + ;; disambiguate). + (setq last-tag-or-attr-name (match-string 1)) + (goto-char (match-beginning 0))) + ;; Nothing else to look for; give up parsing. + (throw 'match nil))))))))) + +(defun js--disambiguate-js-from-jsx (start end) + "Figure out which ‘<’ and ‘>’ chars (from START to END) aren’t JSX. + +Later, this info prevents ‘sgml-’ functions from treating some +‘<’ and ‘>’ chars as parts of tokens of SGML tags — a good thing, +since they are serving their usual function as some JS equality +operator or arrow function, instead." + (goto-char start) + (while (re-search-forward "[<>]" end t) + (unless (if (eq (char-before) ?<) (js--disambiguate-beginning-of-jsx-tag) + (js--disambiguate-end-of-jsx-tag)) + ;; Inform sgml- functions that this >, >=, >>>, <, <=, <<<, or + ;; => token is punctuation (and not an open or close parenthesis + ;; as per usual in sgml-mode). + (put-text-property (1- (point)) (point) 'syntax-table '(1))))) + (defun js-syntax-propertize (start end) ;; JavaScript allows immediate regular expression objects, written /.../. (goto-char start) @@ -1758,7 +1855,8 @@ This performs fontification according to `js--class-styles'." 'syntax-table (string-to-syntax "\"/")) (js-syntax-propertize-regexp end))))) ("\\`\\(#\\)!" (1 "< b"))) - (point) end)) + (point) end) + (if js-jsx-syntax (js--disambiguate-js-from-jsx start end))) (defconst js--prettify-symbols-alist '(("=>" . ?⇒) |