emacs-31 cc210dbf14d: Fix JSX tag angle-bracket pairing in tsx-ts-mode

Sean Whitton <[email protected]> Wed, 5 Aug 2026 06:23:08 -0400 (EDT)
Newsgroups gmane.emacs.diffs
Message-ID <[email protected]>
branch: emacs-31
commit cc210dbf14d5430b988dcf30662914012eced278
Author: Binbin Ye <[email protected]>
Commit: Sean Whitton <[email protected]>

    Fix JSX tag angle-bracket pairing in tsx-ts-mode
    
    * lisp/progmodes/typescript-ts-mode.el (tsx-ts--s-p-query):
    Capture 'jsx_self_closing_element' as well.
    (tsx-ts--syntax-propertize-captures): For JSX tag elements, give
    only the leading '<' and trailing '>' matching-pair syntax and
    leave the interior untouched; for 'jsx_text', neutralize
    balanced pair characters to punctuation (bug#81460).
    * test/lisp/progmodes/typescript-ts-mode-tests.el
    (tsx-ts-mode-test-jsx-tag-syntax-propertize): New test.
---
 lisp/progmodes/typescript-ts-mode.el            | 48 +++++++++++++++----------
 test/lisp/progmodes/typescript-ts-mode-tests.el | 48 +++++++++++++++++++++++++
 2 files changed, 78 insertions(+), 18 deletions(-)

diff --git a/lisp/progmodes/typescript-ts-mode.el b/lisp/progmodes/typescript-ts-mode.el
index dc951811bb3..83a996ad629 100644
--- a/lisp/progmodes/typescript-ts-mode.el
+++ b/lisp/progmodes/typescript-ts-mode.el
@@ -824,7 +824,8 @@ at least 3 (which is the default value)."
                            '(((regex pattern: (regex_pattern) @regexp))
                              ((jsx_text) @jsx)
                              ((jsx_opening_element) @jsx)
-                             ((jsx_closing_element) @jsx)))))
+                             ((jsx_closing_element) @jsx)
+                             ((jsx_self_closing_element) @jsx)))))
 
 (defun typescript-ts--syntax-propertize (beg end)
   (let ((captures (treesit-query-capture 'typescript typescript-ts--s-p-query beg end)))
@@ -845,24 +846,35 @@ at least 3 (which is the default value)."
            (incf ne)
            (put-text-property ns (1+ ns) 'syntax-table syntax)
            (put-text-property (1- ne) ne 'syntax-table syntax)))
-        ;; We put punctuation syntax on all the balanced pair
-        ;; characters so they don't mess up syntax-ppss.  We can't put
-        ;; string syntax on the whole thing because a) it doesn't work
-        ;; if the text is one character long, and b) it interferes
-        ;; forward/backward-sexp.
+
         ('jsx
-         (save-excursion
-           (goto-char ns)
-           (while (re-search-forward (rx (or "{" "}" "[" "]"
-                                             "(" ")" "<" ">"))
-                                     ne t)
-             (put-text-property
-              (match-beginning 0) (match-end 0)
-              'syntax-table (string-to-syntax
-                             (cond
-                              ((equal (match-string 0) "<") "(>")
-                              ((equal (match-string 0) ">") ")<")
-                              (t ".")))))))))))
+         (if (member (treesit-node-type node)
+                     '("jsx_opening_element"
+                       "jsx_closing_element"
+                       "jsx_self_closing_element"))
+             ;; A JSX tag's only real delimiters are its own outermost
+             ;; '<' and '>'.  Mark just those two as a matching pair and
+             ;; leave the interior alone: attribute values are '{...}'
+             ;; expressions of ordinary code (possibly with nested JSX)
+             ;; whose brackets must keep their normal syntax so they
+             ;; nest, match and highlight correctly.
+             (progn
+               (put-text-property ns (1+ ns)
+                                  'syntax-table (string-to-syntax "(>"))
+               (put-text-property (1- ne) ne
+                                  'syntax-table (string-to-syntax ")<")))
+           ;; jsx_text is raw text with no nesting to preserve, so
+           ;; neutralize its balanced-pair characters to punctuation;
+           ;; otherwise stray or unbalanced brackets in the text would
+           ;; confuse syntax-ppss.
+           (save-excursion
+             (goto-char ns)
+             (while (re-search-forward (rx (or "{" "}" "[" "]"
+                                               "(" ")" "<" ">"))
+                                       ne t)
+               (put-text-property
+                (match-beginning 0) (match-end 0)
+                'syntax-table (string-to-syntax "."))))))))))
 
 ;;;###autoload
 (defun tsx-ts-mode-maybe ()
diff --git a/test/lisp/progmodes/typescript-ts-mode-tests.el b/test/lisp/progmodes/typescript-ts-mode-tests.el
index 977a0f44a20..1b065c409fe 100644
--- a/test/lisp/progmodes/typescript-ts-mode-tests.el
+++ b/test/lisp/progmodes/typescript-ts-mode-tests.el
@@ -28,5 +28,53 @@
                     (treesit-ready-p 'tsx)))
   (ert-test-erts-file (ert-resource-file "indent.erts")))
 
+(ert-deftest tsx-ts-mode-test-jsx-tag-syntax-propertize ()
+  "Every JSX tag's angle brackets must pair with that tag's own bounds.
+
+Make sure jsx element's `<' pair with the closing `>' correctly instead
+of the wrong `>' from its attributes contained an arrow function `=>'."
+  (skip-unless (treesit-ready-p 'tsx))
+  (dolist (src '(;; A fragment, arrow-function attributes, and an arrow
+                 ;; that returns a nested `<div></div>' element -- all
+                 ;; inside the opening tag's range.
+                 "const A = () => (
+  <>
+    <Component
+      onClick={() => {}}
+      panel={() => {
+        return <div></div>;
+      }}
+    >
+      {children}
+    </Component>
+  </>
+);
+"
+                 ;; A self-closing element nested inside an attribute
+                 ;; expression.
+                 "const B = () => (
+  <Component slot={<Panel onClick={() => cb()}/>}>
+    {children}
+  </Component>
+);
+"))
+    (with-temp-buffer
+      (insert src)
+      (tsx-ts-mode)
+      (syntax-propertize (point-max))
+      (pcase-dolist (`(,_ . ,node)
+                     (treesit-query-capture
+                      'tsx '(((jsx_opening_element) @el)
+                             ((jsx_closing_element) @el)
+                             ((jsx_self_closing_element) @el))))
+        (let ((ns (treesit-node-start node))
+              (ne (treesit-node-end node)))
+          (should (eq (char-after ns) ?<))
+          (should (eq (char-before ne) ?>))
+          ;; Forward from the tag's `<' lands exactly after its own
+          ;; `>', and backward round-trips to the `<'.
+          (should (= (scan-sexps ns 1) ne))
+          (should (= (scan-sexps ne -1) ns)))))))
+
 (provide 'typescript-ts-mode-tests)
 ;;; typescript-ts-mode-tests.el ends here