diff options
| author | Jackson Ray Hamilton | 2019-02-11 03:00:34 -0800 |
|---|---|---|
| committer | Jackson Ray Hamilton | 2019-04-08 22:48:21 -0700 |
| commit | be86ece42cbb6204480c794d018b02fbda74689b (patch) | |
| tree | a4734ce3c80cfc605851c11c320f544135c4d968 | |
| parent | 27e9bce77db54464737aa5be1ce7142b55f25952 (diff) | |
| download | emacs-be86ece42cbb6204480c794d018b02fbda74689b.tar.gz emacs-be86ece42cbb6204480c794d018b02fbda74689b.zip | |
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.
| -rw-r--r-- | lisp/progmodes/js.el | 100 | ||||
| -rw-r--r-- | test/manual/indent/js-jsx-unclosed-2.js | 14 |
2 files changed, 113 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 @@ | |||
| 82 | (concat js--name-re "\\(?:\\." js--name-re "\\)*") | 82 | (concat js--name-re "\\(?:\\." js--name-re "\\)*") |
| 83 | "Regexp matching a dot-separated sequence of JavaScript names.") | 83 | "Regexp matching a dot-separated sequence of JavaScript names.") |
| 84 | 84 | ||
| 85 | (defconst js--dotted-captured-name-re | ||
| 86 | (concat "\\(" js--name-re "\\)\\(?:\\." js--name-re "\\)*") | ||
| 87 | "Like `js--dotted-name-re', but capture the first name.") | ||
| 88 | |||
| 85 | (defconst js--cpp-name-re js--name-re | 89 | (defconst js--cpp-name-re js--name-re |
| 86 | "Regexp matching a C preprocessor name.") | 90 | "Regexp matching a C preprocessor name.") |
| 87 | 91 | ||
| @@ -1731,6 +1735,99 @@ This performs fontification according to `js--class-styles'." | |||
| 1731 | 'syntax-table (string-to-syntax "\"/")) | 1735 | 'syntax-table (string-to-syntax "\"/")) |
| 1732 | (goto-char end))))) | 1736 | (goto-char end))))) |
| 1733 | 1737 | ||
| 1738 | (defconst js--unary-keyword-re | ||
| 1739 | (js--regexp-opt-symbol '("await" "delete" "typeof" "void" "yield")) | ||
| 1740 | "Regexp matching unary operator keywords.") | ||
| 1741 | |||
| 1742 | (defun js--unary-keyword-p (string) | ||
| 1743 | "Check if STRING is a unary operator keyword in JavaScript." | ||
| 1744 | (string-match-p js--unary-keyword-re string)) | ||
| 1745 | |||
| 1746 | (defun js--disambiguate-beginning-of-jsx-tag () | ||
| 1747 | "Parse enough to determine if a JSX tag starts here. | ||
| 1748 | Disambiguate JSX from equality operators by testing for syntax | ||
| 1749 | only valid as JSX." | ||
| 1750 | ;; “</…” - a JSXClosingElement. | ||
| 1751 | ;; “<>” - a JSXOpeningFragment. | ||
| 1752 | (if (memq (char-after) '(?\/ ?\>)) t | ||
| 1753 | (save-excursion | ||
| 1754 | (skip-chars-forward " \t\n") | ||
| 1755 | (and | ||
| 1756 | (looking-at js--dotted-captured-name-re) | ||
| 1757 | ;; Don’t match code like “if (i < await foo)” | ||
| 1758 | (not (js--unary-keyword-p (match-string 1))) | ||
| 1759 | (progn | ||
| 1760 | (goto-char (match-end 0)) | ||
| 1761 | (skip-chars-forward " \t\n") | ||
| 1762 | (or | ||
| 1763 | ;; “>”, “/>” - tag enders. | ||
| 1764 | ;; “{” - a JSXExpressionContainer. | ||
| 1765 | (memq (char-after) '(?\> ?\/ ?\{)) | ||
| 1766 | ;; Check if a JSXAttribute follows. | ||
| 1767 | (looking-at js--name-start-re))))))) | ||
| 1768 | |||
| 1769 | (defun js--disambiguate-end-of-jsx-tag () | ||
| 1770 | "Parse enough to determine if a JSX tag ends here. | ||
| 1771 | Disambiguate JSX from equality operators by testing for syntax | ||
| 1772 | only valid as JSX, or extremely unlikely except as JSX." | ||
| 1773 | (save-excursion | ||
| 1774 | (backward-char) | ||
| 1775 | ;; “…/>” - a self-closing JSXOpeningElement. | ||
| 1776 | ;; “</>” - a JSXClosingFragment. | ||
| 1777 | (if (= (char-before) ?/) t | ||
| 1778 | (let (last-tag-or-attr-name last-non-unary-p) | ||
| 1779 | (catch 'match | ||
| 1780 | (while t | ||
| 1781 | (skip-chars-backward " \t\n") | ||
| 1782 | ;; Check if the end of a JSXAttribute value or | ||
| 1783 | ;; JSXExpressionContainer almost certainly precedes. | ||
| 1784 | ;; The only valid JS this misses is | ||
| 1785 | ;; - {} > foo | ||
| 1786 | ;; - "bar" > foo | ||
| 1787 | ;; which is no great loss, IMHO… | ||
| 1788 | (if (memq (char-before) '(?\} ?\" ?\' ?\`)) (throw 'match t) | ||
| 1789 | (if (and last-tag-or-attr-name last-non-unary-p | ||
| 1790 | ;; “<”, “</” - tag starters. | ||
| 1791 | (memq (char-before) '(?\< ?\/))) | ||
| 1792 | ;; Leftmost name parsed was the name of a | ||
| 1793 | ;; JSXOpeningElement. | ||
| 1794 | (throw 'match t)) | ||
| 1795 | ;; Technically the dotted name could span multiple | ||
| 1796 | ;; lines, but dear God WHY?! Also, match greedily to | ||
| 1797 | ;; ensure the entire name is valid. | ||
| 1798 | (if (looking-back js--dotted-captured-name-re (point-at-bol) t) | ||
| 1799 | (if (and (setq last-non-unary-p (not (js--unary-keyword-p (match-string 1)))) | ||
| 1800 | last-tag-or-attr-name) | ||
| 1801 | ;; Valid (non-unary) name followed rightwards by | ||
| 1802 | ;; another name (any will do, including | ||
| 1803 | ;; keywords) is invalid JS, but valid JSX. | ||
| 1804 | (throw 'match t) | ||
| 1805 | ;; Remember match and skip backwards over it when | ||
| 1806 | ;; it is the first matched name or the N+1th | ||
| 1807 | ;; matched unary name (unary names on the left are | ||
| 1808 | ;; still ambiguously JS or JSX, so keep parsing to | ||
| 1809 | ;; disambiguate). | ||
| 1810 | (setq last-tag-or-attr-name (match-string 1)) | ||
| 1811 | (goto-char (match-beginning 0))) | ||
| 1812 | ;; Nothing else to look for; give up parsing. | ||
| 1813 | (throw 'match nil))))))))) | ||
| 1814 | |||
| 1815 | (defun js--disambiguate-js-from-jsx (start end) | ||
| 1816 | "Figure out which ‘<’ and ‘>’ chars (from START to END) aren’t JSX. | ||
| 1817 | |||
| 1818 | Later, this info prevents ‘sgml-’ functions from treating some | ||
| 1819 | ‘<’ and ‘>’ chars as parts of tokens of SGML tags — a good thing, | ||
| 1820 | since they are serving their usual function as some JS equality | ||
| 1821 | operator or arrow function, instead." | ||
| 1822 | (goto-char start) | ||
| 1823 | (while (re-search-forward "[<>]" end t) | ||
| 1824 | (unless (if (eq (char-before) ?<) (js--disambiguate-beginning-of-jsx-tag) | ||
| 1825 | (js--disambiguate-end-of-jsx-tag)) | ||
| 1826 | ;; Inform sgml- functions that this >, >=, >>>, <, <=, <<<, or | ||
| 1827 | ;; => token is punctuation (and not an open or close parenthesis | ||
| 1828 | ;; as per usual in sgml-mode). | ||
| 1829 | (put-text-property (1- (point)) (point) 'syntax-table '(1))))) | ||
| 1830 | |||
| 1734 | (defun js-syntax-propertize (start end) | 1831 | (defun js-syntax-propertize (start end) |
| 1735 | ;; JavaScript allows immediate regular expression objects, written /.../. | 1832 | ;; JavaScript allows immediate regular expression objects, written /.../. |
| 1736 | (goto-char start) | 1833 | (goto-char start) |
| @@ -1758,7 +1855,8 @@ This performs fontification according to `js--class-styles'." | |||
| 1758 | 'syntax-table (string-to-syntax "\"/")) | 1855 | 'syntax-table (string-to-syntax "\"/")) |
| 1759 | (js-syntax-propertize-regexp end))))) | 1856 | (js-syntax-propertize-regexp end))))) |
| 1760 | ("\\`\\(#\\)!" (1 "< b"))) | 1857 | ("\\`\\(#\\)!" (1 "< b"))) |
| 1761 | (point) end)) | 1858 | (point) end) |
| 1859 | (if js-jsx-syntax (js--disambiguate-js-from-jsx start end))) | ||
| 1762 | 1860 | ||
| 1763 | (defconst js--prettify-symbols-alist | 1861 | (defconst js--prettify-symbols-alist |
| 1764 | '(("=>" . ?⇒) | 1862 | '(("=>" . ?⇒) |
diff --git a/test/manual/indent/js-jsx-unclosed-2.js b/test/manual/indent/js-jsx-unclosed-2.js index 2d42cf70f84..8b6f33325d7 100644 --- a/test/manual/indent/js-jsx-unclosed-2.js +++ b/test/manual/indent/js-jsx-unclosed-2.js | |||
| @@ -15,3 +15,17 @@ if (foo > bar) void 0 | |||
| 15 | // Don’t even misinterpret unary operators as JSX. | 15 | // Don’t even misinterpret unary operators as JSX. |
| 16 | if (foo < await bar) void 0 | 16 | if (foo < await bar) void 0 |
| 17 | while (await foo > bar) void 0 | 17 | while (await foo > bar) void 0 |
| 18 | |||
| 19 | // Allow unary keyword names as null-valued JSX attributes. | ||
| 20 | // (As if this will EVER happen…) | ||
| 21 | <Foo yield> | ||
| 22 | <Bar void> | ||
| 23 | <Baz | ||
| 24 | zorp | ||
| 25 | typeof> | ||
| 26 | <Please do_n0t delete this_stupidTest > | ||
| 27 | How would we ever live without unary support | ||
| 28 | </Please> | ||
| 29 | </Baz> | ||
| 30 | </Bar> | ||
| 31 | </Foo> | ||