Subject: bug#73779: 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, 19 Oct 2024 14:13:33 +0200

> I was about to install this, but byte-compiling the modified file
> yields the following warnings:
> 
>     ELC      progmodes/php-ts-mode.elc
> 
>   In php-ts-mode--open-statement-group-heuristic:
>   progmodes/php-ts-mode.el:435:58: Warning: Unused lexical argument `parent'
> progmodes/php-ts-mode.el:499:50: Warning: argument `_bol' not left unused
> progmodes/php-ts-mode.el:518:56: Warning: argument `_bol' not left unused
> 
> Could you please fix these and resubmit the patch?

Sorry Eli, it's okay now.
Thank you.

Vincenzo

>From fdf80d7e8db62ad17c70fc52eebd531c7d504206 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 | 170 ++++++++++++++++++++++------------
 1 file changed, 113 insertions(+), 57 deletions(-)

diff --git a/lisp/progmodes/php-ts-mode.el b/lisp/progmodes/php-ts-mode.el
index d2559e5a45f..1c5fdb6f617 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
+  (regexp-opt '("if" "for" "foreach" "while" "do") 'symbols)
+  "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.
@@ -475,43 +496,50 @@ php-ts-mode--parent-eol
     (goto-char (treesit-node-start parent))
     (line-end-position)))
 
-(defun php-ts-mode--parent-html-bol (node parent _bol &rest _)
+(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)))))
-
-(defun php-ts-mode--parent-html-heuristic (node parent _bol &rest _)
+  (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