Subject: 30.0.91; [PATCH] php-ts-mode: Better indentation and font locking, support for the latest grammar version.

From: Vincenzo Pupillo <[email-masked]>
Date: Sat, 12 Oct 2024 22:12:30 +0200

Ciao,
this patch corrects the indentation of the control structure without closing 
brackets (or without the end keyword... in the case of the alternative 
syntax). It supports font locking of the “asymmetric property visibility ” 
introduced with grammar v0.23.3 and so-called magic methods. And others small 
fix. And others small bug I discovered while writing the tests..

Thank you.
Vincenzo

>From 5164e387c395d64b49a851b179ed92847a1cfd93 Mon Sep 17 00:00:00 2001
From: Vincenzo Pupillo <[email-masked]>
Date: Thu, 10 Oct 2024 16:06:37 +0200
Subject: [PATCH] Fix php-ts-mode better indentation and font locking.

Incomplete compound_statement or colon_block (statement-group without a
closing brace or closing keyword) that are not inside a function or
method are not recognized as such by tree-sitter-php.
A new function 'php-ts-mode--open-statement-group-heuristic' handles
this case.  Font locking of magic methods and better support for
alternative control structure syntax.
Support for latest grammar version.

* lisp/progmodes/php-ts-mode.el
(php-ts-mode--language-source-alist): Updated grammar version.
(php-ts-mode--possibly-braceless-keyword-re): Regular expression for
braceles keyword.
(php-ts-mode--open-statement-group-heuristic): New function.
(php-ts-mode--parent-html-bol): Use the new function and doc fix.
(php-ts-mode--parent-html-heuristic): Use the new function and doc fix.
(php-ts-mode--indent-styles): Use the new function and add 'colon_block'
support.
(php-ts-mode--class-magic-methods): New predefined magic methods list.
(php-ts-mode--test-namespace-name-as-prefix-p): Doc fix.
(php-ts-mode--test-namespace-aliasing-clause-p): Fix the test and doc.
(php-ts-mode--test-namespace-use-group-clause-p): Doc fix.
(php-ts-mode--test-visibility-modifier-operation-clause-p): New function
for the new asymmetric property visibility feature of PHP 8.4.
(php-ts-mode--font-lock-settings): Font lock for class magic methods and
alternative syntax.  Better font lock for 'instanceof'.  Use
'font-lock-function-call-face' for scoped and member call expression.
---
 lisp/progmodes/php-ts-mode.el | 164 +++++++++++++++++++++++-----------
 1 file changed, 110 insertions(+), 54 deletions(-)

diff --git a/lisp/progmodes/php-ts-mode.el b/lisp/progmodes/php-ts-mode.el
index d2559e5a45f..f1c33750f47 100644
--- a/lisp/progmodes/php-ts-mode.el
+++ b/lisp/progmodes/php-ts-mode.el
@@ -84,7 +84,7 @@
 
 ;;; Install treesitter language parsers
 (defvar php-ts-mode--language-source-alist
-  '((php . ("https://github.com/tree-sitter/tree-sitter-php" "v0.23.0" "php/src"))
+  '((php . ("https://github.com/tree-sitter/tree-sitter-php" "v0.23.4" "php/src"))
     (phpdoc . ("https://github.com/claytonrcarter/tree-sitter-phpdoc"))
     (html . ("https://github.com/tree-sitter/tree-sitter-html"  "v0.23.0"))
     (javascript . ("https://github.com/tree-sitter/tree-sitter-javascript" "v0.23.0"))
@@ -428,6 +428,27 @@ php-ts-mode--syntax-table
 
 ;;; Indent
 
+(defconst php-ts-mode--possibly-braceless-keyword-re
+  (concat "\\_<" (regexp-opt '("if" "for" "foreach" "while" "do") t) "\\_>")
+  "Regexp matching keywords optionally followed by an opening brace.")
+
+(defun php-ts-mode--open-statement-group-heuristic (node parent bol &rest _)
+  "Heuristic matcher for statement-group without closing bracket.
+
+Return `php-ts-mode-indent-offset' plus 1 when BOL is after
+`php-ts-mode--possibly-braceless-keyword-re', otherwise return 0.  It's
+usefull for matching incomplete compound_statement or colon_block.
+PARENT is NODE's parent, BOL is the beginning of non-whitespace
+characters of the current line."
+  (and (null node)
+       (save-excursion
+	   (forward-line -1)
+	   (if (re-search-forward
+		php-ts-mode--possibly-braceless-keyword-re
+		bol t)
+	       (+ 1 php-ts-mode-indent-offset)
+	     0))))
+
 ;; taken from c-ts-mode
 (defun php-ts-mode--else-heuristic (node parent bol &rest _)
   "Heuristic matcher for when \"else\" is followed by a closing bracket.
@@ -478,40 +499,47 @@ php-ts-mode--parent-eol
 (defun php-ts-mode--parent-html-bol (node parent _bol &rest _)
   "Find the first non-space characters of the HTML tags before NODE.
 
+When NODE is nil call `php-ts-mode--open-statement-group-heuristic'.
 PARENT is NODE's parent, BOL is the beginning of non-whitespace
 characters of the current line."
-  (save-excursion
-    (let ((html-node (treesit-search-forward node "text" t)))
-      (if html-node
-          (let ((end-html (treesit-node-end html-node)))
-            (goto-char end-html)
-            (backward-word)
-            (back-to-indentation)
-            (point))
-        (treesit-node-start parent)))))
+  (if (null node)
+      ;; If NODE is nil it could be an open statement-group.
+      (php-ts-mode--open-statement-group-heuristic node parent _bol)
+    (save-excursion
+      (let ((html-node (treesit-search-forward node "text" t)))
+	(if html-node
+            (let ((end-html (treesit-node-end html-node)))
+              (goto-char end-html)
+              (backward-word)
+              (back-to-indentation)
+              (point))
+          (treesit-node-start parent))))))
 
 (defun php-ts-mode--parent-html-heuristic (node parent _bol &rest _)
   "Return position based on html indentation.
 
 Returns 0 if the NODE is after the </html>, otherwise returns the
-indentation point of the last word before the NODE, plus the
-indentation offset.  If there is no HTML tag, it returns the beginning
-of the parent.
+indentation point of the last word before the NODE, plus the indentation
+offset.  If there is no HTML tag, it returns the beginning of the
+parent.  When NODE is nil call `php-ts-mode--open-statement-group-heuristic'.
 It can be used when you want to indent PHP code relative to the HTML.
 PARENT is NODE's parent, BOL is the beginning of non-whitespace
 characters of the current line."
-  (let ((html-node (treesit-search-forward node "text" t)))
-    (if html-node
-        (let ((end-html (treesit-node-end html-node)))
-          (save-excursion
-            (goto-char end-html)
-            (backward-word)
-            (back-to-indentation)
-            (if (search-forward "</html>" end-html t 1)
-                0
-              (+ (point) php-ts-mode-indent-offset))))
-      ;; Maybe it's better to use bol?
-      (treesit-node-start parent))))
+  (if (null node)
+      ;; If NODE is nil it could be an open statement-group.
+      (php-ts-mode--open-statement-group-heuristic node parent _bol)
+    (let ((html-node (treesit-search-forward node "text" t)))
+      (if html-node
+          (let ((end-html (treesit-node-end html-node)))
+            (save-excursion
+	      (goto-char end-html)
+	      (backward-word)
+	      (back-to-indentation)
+	      (if (search-forward "</html>" end-html t 1)
+                  0
+		(+ (point) php-ts-mode-indent-offset))))
+	;; Maybe it's better to use bol?
+	(treesit-node-start parent)))))
 
 (defun php-ts-mode--array-element-heuristic (_node parent _bol &rest _)
   "Return of the position of the first element of the array.
@@ -648,16 +676,22 @@ php-ts-mode--indent-styles
            ((parent-is "initializer_list") parent-bol php-ts-mode-indent-offset)
 
            ;; Statement in {} blocks.
-           ((or (and (parent-is "compound_statement")
+           ((or (and (or (parent-is "compound_statement")
+			 (parent-is "colon_block"))
                      ;; If the previous sibling(s) are not on their
                      ;; own line, indent as if this node is the first
                      ;; sibling
                      php-ts-mode--first-sibling)
-                (match null "compound_statement"))
+                (or (match null "compound_statement")
+		    (match null "colon_block")))
             standalone-parent php-ts-mode-indent-offset)
-           ((parent-is "compound_statement") parent-bol php-ts-mode-indent-offset)
+           ((or (parent-is "compound_statement")
+		(parent-is "colon_block"))
+	    parent-bol php-ts-mode-indent-offset)
            ;; Opening bracket.
-           ((node-is "compound_statement") standalone-parent php-ts-mode-indent-offset)
+           ((or (node-is "compound_statement")
+                (node-is "colon_block"))
+                standalone-parent php-ts-mode-indent-offset)
 
            ((parent-is "match_block") parent-bol php-ts-mode-indent-offset)
            ((parent-is "switch_block") parent-bol 0)
@@ -667,6 +701,7 @@ php-ts-mode--indent-styles
            ;; rule for PHP alternative syntax
            ((or (node-is "else_if_clause")
                 (node-is "endif")
+                (node-is "endfor")
                 (node-is "endforeach")
                 (node-is "endwhile"))
             parent-bol 0)
@@ -679,9 +714,13 @@ php-ts-mode--indent-styles
                 (parent-is "switch_statement")
                 (parent-is "case_statement")
                 (parent-is "empty_statement"))
-            parent-bol php-ts-mode-indent-offset))))
+            parent-bol php-ts-mode-indent-offset)
+
+	   ;; Workaround: handle "for" open statement group. Currently
+           ;; the grammar handles it differently than other control structures.
+	   (no-node php-ts-mode--open-statement-group-heuristic 0))))
     `((psr2
-       ((parent-is "program") parent-bol 0)
+       ((parent-is "program") php-ts-mode--open-statement-group-heuristic 0)
        ((parent-is "text_interpolation") column-0 0)
        ((parent-is "function_call_expression") parent-bol php-ts-mode-indent-offset)
        ,@common)
@@ -774,21 +813,32 @@ php-ts-mode--predefined-constant
     "__FUNCTION__" "__LINE__" "__METHOD__" "__NAMESPACE__" "__TRAIT__")
   "PHP predefined constant.")
 
-(defun php-ts-mode--test-namespace-name-as-prefix-p  ()
-  "Return t if namespace_name_as_prefix keyword is a named node, nil otherwise."
+(defconst php-ts-mode--class-magic-methods
+  '("__construct" "__destruct" "__call" "__callStatic" "__get" "__set"
+    "__isset" "__unset" "__sleep" "__wakeup" "__serialize" "__unserialize"
+    "__toString" "__invoke" "__set_state" "__clone" "__debugInfo")
+  "PHP predefined magic methods.")
+
+(defun php-ts-mode--test-namespace-name-as-prefix-p ()
+  "Return t if namespace_name_as_prefix is a named node, nil otherwise."
   (ignore-errors
     (progn (treesit-query-compile 'php "(namespace_name_as_prefix)" t) t)))
 
-(defun php-ts-mode--test-namespace-aliasing-clause-p  ()
-  "Return t if namespace_name_as_prefix keyword is named node, nil otherwise."
+(defun php-ts-mode--test-namespace-aliasing-clause-p ()
+  "Return t if namespace_aliasing_clause is a named node, nil otherwise."
   (ignore-errors
-    (progn (treesit-query-compile 'php "(namespace_name_as_prefix)" t) t)))
+    (progn (treesit-query-compile 'php "(namespace_aliasing_clause)" t) t)))
 
 (defun php-ts-mode--test-namespace-use-group-clause-p ()
-  "Return t if namespace_use_group_clause keyword is named node, nil otherwise."
+  "Return t if namespace_use_group_clause is a named node, nil otherwise."
   (ignore-errors
     (progn (treesit-query-compile 'php "(namespace_use_group_clause)" t) t)))
 
+(defun php-ts-mode--test-visibility-modifier-operation-clause-p ()
+  "Return t if (visibility_modifier (operation)) is defined, nil otherwise."
+  (ignore-errors
+    (progn (treesit-query-compile 'php "(visibility_modifier (operation))" t) t)))
+
 (defun php-ts-mode--font-lock-settings ()
   "Tree-sitter font-lock settings."
   (treesit-font-lock-rules
@@ -796,7 +846,10 @@ php-ts-mode--font-lock-settings
    :language 'php
    :feature 'keyword
    :override t
-   `([,@php-ts-mode--keywords] @font-lock-keyword-face)
+   `([,@php-ts-mode--keywords] @font-lock-keyword-face
+     ,@(when (php-ts-mode--test-visibility-modifier-operation-clause-p)
+	 '((visibility_modifier (operation) @font-lock-builtin-face)))
+     (var_modifier) @font-lock-builtin-face)
 
    :language 'php
    :feature 'comment
@@ -826,7 +879,6 @@ php-ts-mode--font-lock-settings
      (named_label_statement (name) @font-lock-constant-face))
 
    :language 'php
-   ;;:override t
    :feature 'delimiter
    `((["," ":" ";" "\\"]) @font-lock-delimiter-face)
 
@@ -850,7 +902,6 @@ php-ts-mode--font-lock-settings
 
    :language 'php
    :feature 'string
-   ;;:override t
    `(("\"") @font-lock-string-face
      (encapsed_string) @font-lock-string-face
      (string_content) @font-lock-string-face
@@ -892,32 +943,37 @@ php-ts-mode--font-lock-settings
       name: (_) @font-lock-type-face)
      (trait_declaration
       name: (_) @font-lock-type-face)
-     (property_declaration
-      (visibility_modifier) @font-lock-keyword-face)
-     (property_declaration
-      (var_modifier) @font-lock-keyword-face)
      (enum_declaration
       name: (_) @font-lock-type-face)
      (function_definition
       name: (_) @font-lock-function-name-face)
      (method_declaration
       name: (_) @font-lock-function-name-face)
+     (method_declaration
+      name: (name) @font-lock-builtin-face
+      (:match ,(rx-to-string
+                `(: bos (or ,@php-ts-mode--class-magic-methods) eos))
+              @font-lock-builtin-face))
      ("=>") @font-lock-keyword-face
      (object_creation_expression
       (name) @font-lock-type-face)
      ,@(when (php-ts-mode--test-namespace-name-as-prefix-p)
-	   '((namespace_name_as_prefix "\\" @font-lock-delimiter-face)
-	     (namespace_name_as_prefix
-	      (namespace_name (name)) @font-lock-type-face)))
+           '((namespace_name_as_prefix "\\" @font-lock-delimiter-face)
+             (namespace_name_as_prefix
+              (namespace_name (name)) @font-lock-type-face)))
      ,@(if (php-ts-mode--test-namespace-aliasing-clause-p)
-	   '((namespace_aliasing_clause (name) @font-lock-type-face))
-	 '((namespace_use_clause alias: (name) @font-lock-type-face)))
+           '((namespace_aliasing_clause (name) @font-lock-type-face))
+         '((namespace_use_clause alias: (name) @font-lock-type-face)))
      ,@(when (not (php-ts-mode--test-namespace-use-group-clause-p))
-	 '((namespace_use_group
-	    (namespace_use_clause (name) @font-lock-type-face))))
+         '((namespace_use_group
+            (namespace_use_clause (name) @font-lock-type-face))))
      (namespace_name "\\" @font-lock-delimiter-face)
      (namespace_name (name) @font-lock-type-face)
-     (use_declaration (name) @font-lock-property-use-face))
+     (use_declaration (name) @font-lock-property-use-face)
+     (use_instead_of_clause (name) @font-lock-type-face)
+     (binary_expression
+      operator: "instanceof"
+      right: (name) @font-lock-type-face))
 
    :language 'php
    :feature 'function-scope
@@ -932,9 +988,9 @@ php-ts-mode--font-lock-settings
    '((function_call_expression
       function: (name) @font-lock-function-call-face)
      (scoped_call_expression
-      name: (_) @font-lock-function-name-face)
+      name: (_) @font-lock-function-call-face)
      (member_call_expression
-      name: (_) @font-lock-function-name-face)
+      name: (_) @font-lock-function-call-face)
      (nullsafe_member_call_expression
       name: (_) @font-lock-constant-face))
 
-- 
2.47.0