Subject: Re: bug#79339: 31.0.50; [PATCH] php-ts-mode: support PHP 8.5

From: Vincenzo Pupillo <[email-masked]>
Date: Sun, 07 Sep 2025 19:20:49 +0200

In data sabato 6 settembre 2025 10:56:47 Ora legale dell’Europa centrale, Eli 
Zaretskii ha scritto:
> > From: Vincenzo Pupillo <[email-masked]>
> > Date: Fri, 29 Aug 2025 21:36:07 +0200
> > 
> > Ciao,
> > this patch add support for the version 24.2 of the grammar for PHP 8.5
> > (now in beta 2).
> > In addition to this, there are some fixes for both indentation and syntax
> > highlighting.
> 
> Thanks.
> 
> Is this supposed to keep working with older versions of the grammar?
Yes. I used this patch for a week with the old grammar and didn't encounter 
any problems.

> 
> >  (defconst php-ts-mode--keywords
> > 
> > -  '("abstract" "and" "array" "as" "break" "callable" "case" "catch"
> > -    "class" "clone" "const" "continue" "declare" "default" "do" "echo"
> > -    "else" "elseif" "enddeclare" "endfor" "endforeach" "endif"
> > -    "endswitch" "endwhile" "enum" "exit" "extends" "final" "finally" "fn"
> > -    "for" "foreach" "from" "function" "global" "goto" "if" "implements"
> > -    "include" "include_once" "instanceof" "insteadof" "interface"
> > -    "list" "match" "namespace" "new" "null" "or" "print" "private"
> > -    "protected" "public" "readonly" "require" "require_once" "return"
> > -    "static" "switch" "throw" "trait" "try" "unset" "use" "while" "xor"
> > -    "yield")
> > +  (when (treesit-available-p)
> > +    (append
> > +     '("abstract" "and" "array" "as" "break" "case" "catch"
> > +       "class" "clone" "const" "continue" "declare" "default" "do" "echo"
> > +       "else" "elseif" "enddeclare" "endfor" "endforeach" "endif"
> > +       "endswitch" "endwhile" "enum" "exit" "extends" "final" "finally"
> > "fn" +       "for" "foreach" "function" "global" "goto" "if" "implements"
> > +       "include" "include_once" "instanceof" "insteadof" "interface" +  
> >     "list" "match" "namespace" "new" "null" "or" "print" "private" +     
> >  "protected" "public" "readonly" "require" "require_once" "return" +     
> >  "static" "switch" "throw" "trait" "try" "unset" "use" "while" "xor" +   
> >    "yield")
> > +     (if (php-ts-mode--test-yield-from-p) '("yield from") '("from"))))
> > 
> >    "PHP keywords for tree-sitter font-locking.")
> >  
> >  (defconst php-ts-mode--operators
> > 
> > -  '("--" "**=" "*=" "/=" "%=" "+=" "-=" ".=" "<<=" ">>=" "&=" "^="
> > -    "|=" "??"  "??=" "||" "&&" "|" "^" "&" "==" "!=" "<>" "===" "!=="
> > -    "<" ">" "<=" ">=" "<=>" "<<" ">>" "+" "-" "." "*" "**" "/" "%"
> > -    "->" "?->" "...")
> > +  (when (treesit-available-p)
> > +    (append
> > +     '("--" "**=" "*=" "/=" "%=" "+=" "-=" ".=" "<<=" ">>=" "&=" "^="
> > +       "|=" "??"  "??=" "||" "&&" "|" "^" "&" "==" "!=" "<>" "===" "!=="
> > +       "<" ">" "<=" ">=" "<=>" "<<" ">>" "+" "-" "." "*" "**" "/" "%"
> > +       "->" "?->" "...")
> > +     (when (php-ts-mode--test-pipe-p) '("|>"))))
> > 
> >    "PHP operators for tree-sitter font-locking.")
> 
> Since these two are defconsts, is it wise to have their value depend
> on something that could change with the grammar version?  Doing so
> might mean that the byte-compiled value includes traces of the grammar
> library installed on the system where the file was byte-compiled,
> which might cause trouble if the system on which the code is run has a
> different grammar version.  Or am I missing something?
No, you're right. Here is the new version attached. Now the constants are 
variables.

Thank you.

Vincenzo
From 2286cf033075a49893f569d3d25a26ecd95348ae Mon Sep 17 00:00:00 2001
From: Vincenzo Pupillo <[email-masked]>
Date: Thu, 28 Aug 2025 23:29:30 +0200
Subject: [PATCH] Add support for PHP 8.5.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* lisp/progmodes/php-ts-mode.el: Doc string update.
(php-ts-mode--language-source-alist): PHP grammar update.
(php-ts-mode-find-sibling-rules): Doc string fix.
(php-ts-mode--parent-html-heuristic): Handled the case where there is no
HTML between two blocks of PHP code.
(php-ts-mode--array-element-heuristic): Fix typo.
(php-ts-mode--pipe-heuristic): New function that handle PHP's new pipe
operator.
(php-ts-mode--indent-styles): Removed commented code. More accurate
indentation of ‘member_call_expression’. Use of new function to handle
pipe operator indentation. New rule for indenting ‘property_hook_list’.
(php-ts-mode--test-yield-from-p): New predicate to check whether the
'yield from' keyword is supported by the PHP grammar.
(php-ts-mode--test-pipe-p): New predicate to check whether the ‘pipe’
operator is supported by the PHP grammar.
(php-ts-mode--keywords): Use the new predicate
'php-ts-mode--test-yield-from-p'. Now it is a var instead of a const.
(php-ts-mode--operators): Use the new predicate
'php-ts-mode--test-pipe-p'. Now it is a var instead of a const.
(php-ts-mode--font-lock-settings): New rules for
'nullsafe_member_access_expression', 'callable' primitive type. New rule
for the 'php_end_tag' if supported by the grammar. More precise rules
for 'scoped_call_expression' and 'member_call_expression'.
(php-ts-mode--inferior-php-process): Removed this unnecessary variable
(inferior-php-ts-mode-startup): Removed the use of
'php-ts-mode--inferior-php-process'.
(php-ts-mode-inferior--write-history): Removed the use of
'php-ts-mode--inferior-php-process'.
---
 lisp/progmodes/php-ts-mode.el | 168 +++++++++++++++++++++-------------
 1 file changed, 105 insertions(+), 63 deletions(-)

diff --git a/lisp/progmodes/php-ts-mode.el b/lisp/progmodes/php-ts-mode.el
index e23293bdd3b..3c9cb2ba8d0 100644
--- a/lisp/progmodes/php-ts-mode.el
+++ b/lisp/progmodes/php-ts-mode.el
@@ -30,7 +30,7 @@
 ;; - tree-sitter-jsdoc: v0.23.2
 ;; - tree-sitter-javascript: v0.23.1-2-g108b2d4
 ;; - tree-sitter-html: v0.23.2-1-gd9219ad
-;; - tree-sitter-php: v0.23.12
+;; - tree-sitter-php: v0.24.2
 ;;
 ;; We try our best to make builtin modes work with latest grammar
 ;; versions, so a more recent grammar has a good chance to work too.
@@ -81,7 +81,7 @@
 ;;; Install treesitter language parsers
 (defvar php-ts-mode--language-source-alist
   '((php "https://github.com/tree-sitter/tree-sitter-php"
-	 :commit "f7cf7348737d8cff1b13407a0bfedce02ee7b046"
+	 :commit "5b5627faaa290d89eb3d01b9bf47c3bb9e797dea"
 	 :source-dir "php/src")
     (phpdoc "https://github.com/claytonrcarter/tree-sitter-phpdoc"
 	    :commit "03bb10330704b0b371b044e937d5cc7cd40b4999"))
@@ -238,8 +238,8 @@ php-ts-mode-inferior-history
 (defcustom php-ts-mode-find-sibling-rules
   (list (list (rx "src/" (group (+ not-newline) "/") (group (+ (not "/"))) ".php") "tests/\\1\\2Test.php")
 	(list (rx "tests/" (group (+ not-newline) "/") (group (+ (not "/"))) "Test.php") "src/\\1\\2.php"))
-  "Rules for finding sibling files.  See `find-sibling-rules' for the
-  form of the value.
+  "Rules for finding sibling files.
+See `find-sibling-rules' for the form of the value.
 As a default, the rules try to find the corresponding test of the
 current source file and vice versa.  Source files are assumed to be in
 src/, and tests of in tests/.  Many frameworks have a folder
@@ -587,7 +587,11 @@ php-ts-mode--parent-html-heuristic
      ((eq php-ts-mode-html-relative-indent 'ignore) (line-beginning-position))
      ((search-backward "</html>" (treesit-node-start parent) t 1) (line-beginning-position))
      ((null node) (apply (alist-get 'prev-sibling treesit-simple-indent-presets) node parent bol nil))
-     (t (when-let* ((html-node (treesit-search-forward node "text" t))
+     (t (when-let* ((html-node (treesit-search-forward
+				node
+				(lambda (node)
+				  (equal (treesit-node-type node) "text"))
+				t))
 		    (end-html (treesit-node-end html-node)))
 	  (goto-char end-html)
 	  ;; go to the start of the last tag
@@ -622,7 +626,7 @@ php-ts-mode--array-element-heuristic
 (defun php-ts-mode--anchor-first-sibling (_node parent _bol &rest _)
   "Return the start of the first child of a sibling of PARENT.
 
-If the fist sibling of PARENT and the first child of the sibling are
+If the first sibling of PARENT and the first child of the sibling are
 on the same line return the start position of the first child of the
 sibling.  Otherwise return the start of the first sibling.
 PARENT is NODE's parent, BOL is the beginning of non-whitespace
@@ -663,12 +667,27 @@ php-ts-mode--anchor-prev-sibling
 		(treesit-node-prev-sibling prev-sibling)))))
     (treesit-node-start prev-sibling)))
 
+(defun php-ts-mode--pipe-heuristic (node parent _bol &rest _)
+  "Return the start of the previous pipe to the current pipe NODE.
+Otherwise return the beginning of line of the previous non pipe NODE.
+
+PARENT is NODE's parent, BOL is the beginning of non-whitespace
+characters of the current line."
+  (save-excursion
+    (let* ((parent-start (treesit-node-start parent))
+	   (node-start (treesit-node-start node))
+	   (bound (progn
+		    (goto-char parent-start)
+		    (beginning-of-line 1)
+		    (point))))
+      (goto-char node-start)
+      (let ((previous-pipe (search-backward "|>" bound t nil)))
+	(or previous-pipe (+ bound php-ts-mode-indent-offset))))))
+
 (defun php-ts-mode--indent-styles ()
   "Indent rules supported by `php-ts-mode'."
   (let ((common
 	 `(;; Handle indentation relatives to HTML.
-	   ;; ((parent-is "program") php-ts-mode--parent-html-heuristic 0)
-	   ;; ((parent-is "text_interpolation") php-ts-mode--parent-html-heuristic 0)
 	   ((or (parent-is "program")
 		(parent-is "text_interpolation"))
 	    php-ts-mode--parent-html-heuristic 0)
@@ -709,12 +728,14 @@ php-ts-mode--indent-styles
 		(parent-is "use_list"))
 	    parent-bol php-ts-mode-indent-offset)
 	   ((parent-is "function_definition") parent-bol 0)
-	   ((parent-is "member_call_expression") first-sibling php-ts-mode-indent-offset)
+	   ((parent-is "member_call_expression") parent-bol php-ts-mode-indent-offset)
 	   ((parent-is "conditional_expression") parent-bol php-ts-mode-indent-offset)
 	   ((parent-is "assignment_expression") parent-bol php-ts-mode-indent-offset)
 	   ((parent-is "array_creation_expression") parent-bol php-ts-mode-indent-offset)
 	   ((parent-is "attribute_group") parent-bol php-ts-mode-indent-offset)
 	   ((parent-is "parenthesized_expression") first-sibling 1)
+
+	   ((node-is "|>") php-ts-mode--pipe-heuristic 0)
 	   ((parent-is "binary_expression") parent 0)
 	   ((or (parent-is "arguments")
 		(parent-is "formal_parameters"))
@@ -742,6 +763,7 @@ php-ts-mode--indent-styles
 	   ((parent-is "declaration_list") column-0 php-ts-mode-indent-offset)
 
 	   ((parent-is "initializer_list") parent-bol php-ts-mode-indent-offset)
+	   ((parent-is "property_hook_list") parent-bol php-ts-mode-indent-offset)
 
 	   ;; Statement in {} blocks.
 	   ((or (and (or (parent-is "compound_statement")
@@ -827,24 +849,66 @@ php-ts-mode--phpdoc-indent-rules
 
 ;;; Font-lock
 
-(defconst php-ts-mode--keywords
-  '("abstract" "and" "array" "as" "break" "callable" "case" "catch"
-    "class" "clone" "const" "continue" "declare" "default" "do" "echo"
-    "else" "elseif" "enddeclare" "endfor" "endforeach" "endif"
-    "endswitch" "endwhile" "enum" "exit" "extends" "final" "finally" "fn"
-    "for" "foreach" "from" "function" "global" "goto" "if" "implements"
-    "include" "include_once" "instanceof" "insteadof" "interface"
-    "list" "match" "namespace" "new" "null" "or" "print" "private"
-    "protected" "public" "readonly" "require" "require_once" "return"
-    "static" "switch" "throw" "trait" "try" "unset" "use" "while" "xor"
-    "yield")
+(defun php-ts-mode--test-namespace-name-as-prefix-p ()
+  "Return t if namespace_name_as_prefix is a named node, nil otherwise."
+  (treesit-query-valid-p 'php "(namespace_name_as_prefix)"))
+
+(defun php-ts-mode--test-namespace-aliasing-clause-p ()
+  "Return t if namespace_aliasing_clause is a named node, nil otherwise."
+  (treesit-query-valid-p 'php "(namespace_aliasing_clause)"))
+
+(defun php-ts-mode--test-namespace-use-group-clause-p ()
+  "Return t if namespace_use_group_clause is a named node, nil otherwise."
+  (treesit-query-valid-p 'php "(namespace_use_group_clause)"))
+
+(defun php-ts-mode--test-visibility-modifier-operation-p ()
+  "Return t if (visibility_modifier (operation)) is defined, nil otherwise."
+  (treesit-query-valid-p 'php "(visibility_modifier (operation))"))
+
+(defun php-ts-mode--test-property-hook-p ()
+  "Return t if property_hook is a named node, nil otherwise."
+  (treesit-query-valid-p 'php "(property_hook)"))
+
+(defun php-ts-mode--test-relative-name-p ()
+  "Return t if relative_name is a named node, nil otherwise."
+  (treesit-query-valid-p 'php "(relative_name)"))
+
+(defun php-ts-mode--test-php-end-tag-p ()
+  "Return t if php_end_tag is a named node, nil otherwise."
+  (treesit-query-valid-p 'php "(php_end_tag)"))
+
+(defun php-ts-mode--test-yield-from-p ()
+  "Return t if the keyword `yield from' is defined, nil otherwise."
+  (treesit-query-valid-p 'php '("yield from")))
+
+(defun php-ts-mode--test-pipe-p ()
+  "Return t if the operator '|>' is defined, nil otherwise."
+  (treesit-query-valid-p 'php '("|>")))
+
+(defvar php-ts-mode--keywords
+  (when (treesit-available-p)
+    (append
+     '("abstract" "and" "array" "as" "break" "case" "catch"
+       "class" "clone" "const" "continue" "declare" "default" "do" "echo"
+       "else" "elseif" "enddeclare" "endfor" "endforeach" "endif"
+       "endswitch" "endwhile" "enum" "exit" "extends" "final" "finally" "fn"
+       "for" "foreach" "function" "global" "goto" "if" "implements"
+       "include" "include_once" "instanceof" "insteadof" "interface"
+       "list" "match" "namespace" "new" "null" "or" "print" "private"
+       "protected" "public" "readonly" "require" "require_once" "return"
+       "static" "switch" "throw" "trait" "try" "unset" "use" "while" "xor"
+       "yield")
+     (if (php-ts-mode--test-yield-from-p) '("yield from") '("from"))))
   "PHP keywords for tree-sitter font-locking.")
 
-(defconst php-ts-mode--operators
-  '("--" "**=" "*=" "/=" "%=" "+=" "-=" ".=" "<<=" ">>=" "&=" "^="
-    "|=" "??"  "??=" "||" "&&" "|" "^" "&" "==" "!=" "<>" "===" "!=="
-    "<" ">" "<=" ">=" "<=>" "<<" ">>" "+" "-" "." "*" "**" "/" "%"
-    "->" "?->" "...")
+(defvar php-ts-mode--operators
+  (when (treesit-available-p)
+    (append
+     '("--" "**=" "*=" "/=" "%=" "+=" "-=" ".=" "<<=" ">>=" "&=" "^="
+       "|=" "??"  "??=" "||" "&&" "|" "^" "&" "==" "!=" "<>" "===" "!=="
+       "<" ">" "<=" ">=" "<=>" "<<" ">>" "+" "-" "." "*" "**" "/" "%"
+       "->" "?->" "...")
+     (when (php-ts-mode--test-pipe-p) '("|>"))))
   "PHP operators for tree-sitter font-locking.")
 
 (defconst php-ts-mode--predefined-constant
@@ -890,30 +954,6 @@ php-ts-mode--prettify-symbols-alist
     ("::" . ?∷))
   "Value for `prettify-symbols-alist' in `php-ts-mode'.")
 
-(defun php-ts-mode--test-namespace-name-as-prefix-p ()
-  "Return t if namespace_name_as_prefix is a named node, nil otherwise."
-  (treesit-query-valid-p 'php "(namespace_name_as_prefix)"))
-
-(defun php-ts-mode--test-namespace-aliasing-clause-p ()
-  "Return t if namespace_aliasing_clause is a named node, nil otherwise."
-  (treesit-query-valid-p 'php "(namespace_aliasing_clause)"))
-
-(defun php-ts-mode--test-namespace-use-group-clause-p ()
-  "Return t if namespace_use_group_clause is a named node, nil otherwise."
-  (treesit-query-valid-p 'php "(namespace_use_group_clause)"))
-
-(defun php-ts-mode--test-visibility-modifier-operation-p ()
-  "Return t if (visibility_modifier (operation)) is defined, nil otherwise."
-  (treesit-query-valid-p 'php "(visibility_modifier (operation))"))
-
-(defun php-ts-mode--test-property-hook-p ()
-  "Return t if property_hook is a named node, nil otherwise."
-  (treesit-query-valid-p 'php "(property_hook)"))
-
-(defun php-ts-mode--test-relative-name-p ()
-  "Return t if relative_name is a named node, nil otherwise."
-  (treesit-query-valid-p 'php "(relative_name)"))
-
 (defun php-ts-mode--font-lock-settings ()
   "Tree-sitter font-lock settings."
   (treesit-font-lock-rules
@@ -974,6 +1014,7 @@ php-ts-mode--font-lock-settings
       name: (_) @font-lock-variable-name-face)
      (scoped_property_access_expression
       scope: (name) @font-lock-constant-face)
+     (nullsafe_member_access_expression (name) @font-lock-variable-name-face)
      (error_suppression_expression (name) @font-lock-property-name-face))
 
    :language 'php
@@ -1002,6 +1043,8 @@ php-ts-mode--font-lock-settings
      (union_type) @font-lock-type-face
      (bottom_type) @font-lock-type-face
      (primitive_type) @font-lock-type-face
+     ((primitive_type) @font-lock-keyword-face
+      (:equal "callable" @font-lock-keyword-face))
      (cast_type) @font-lock-type-face
      (named_type) @font-lock-type-face
      (optional_type) @font-lock-type-face)
@@ -1010,7 +1053,9 @@ php-ts-mode--font-lock-settings
    :feature 'definition
    :override t
    `((php_tag) @font-lock-preprocessor-face
-     ("?>") @font-lock-preprocessor-face
+     ,@(if (php-ts-mode--test-php-end-tag-p)
+	   '((php_end_tag) @font-lock-preprocessor-face)
+	 '(("?>") @font-lock-preprocessor-face))
      ;; Highlights identifiers in declarations.
      (class_declaration
       name: (_) @font-lock-type-face)
@@ -1067,9 +1112,9 @@ php-ts-mode--font-lock-settings
    '((function_call_expression
       function: (name) @font-lock-function-call-face)
      (scoped_call_expression
-      name: (_) @font-lock-function-call-face)
+      name: (name) @font-lock-function-call-face)
      (member_call_expression
-      name: (_) @font-lock-function-call-face)
+      name: (name) @font-lock-function-call-face)
      (nullsafe_member_call_expression
       name: (_) @font-lock-function-call-face))
 
@@ -1792,9 +1837,6 @@ inferior-php-ts-mode
 
 ;;; Inferior PHP process.
 
-(defvar php-ts-mode--inferior-php-process nil
-  "The PHP inferior process associated to `php-ts-mode-inferior-php-buffer'.")
-
 ;;;###autoload
 (defun run-php (&optional cmd config)
   "Run an PHP interpreter as a inferior process.
@@ -1829,8 +1871,7 @@ inferior-php-ts-mode-startup
   "Start an inferior PHP process with command CMD and init file CONFIG.
 CMD is the command to run.  Optional CONFIG, if supplied, is the php.ini
 file to use."
-  (setq-local php-ts-mode--inferior-php-process
-	      (apply #'make-comint-in-buffer
+  (apply #'make-comint-in-buffer
 		     (string-replace "*" "" php-ts-mode-inferior-php-buffer)
 		     php-ts-mode-inferior-php-buffer
 		     cmd
@@ -1840,7 +1881,7 @@ inferior-php-ts-mode-startup
 		      (list
 		       (when config
 			 (format "-c %s" config))
-		       "-a"))))
+		       "-a")))
   (add-hook 'comint-preoutput-filter-functions
 	    (lambda (string)
 	      (let ((prompt (concat php-ts-mode--inferior-prompt " ")))
@@ -1865,7 +1906,7 @@ inferior-php-ts-mode-startup
 	    nil t)
   (when php-ts-mode-inferior-history
     (set-process-sentinel
-     (get-buffer-process  php-ts-mode-inferior-php-buffer)
+     (get-buffer-process php-ts-mode-inferior-php-buffer)
      'php-ts-mode-inferior--write-history)))
 
 ;; taken and adapted from lua-ts-mode
@@ -1880,14 +1921,15 @@ php-ts-mode-inferior--write-history
 (defun php-ts-mode-send-region (beg end)
   "Send region between BEG and END to the inferior PHP process."
   (interactive "r")
-  (if (buffer-live-p php-ts-mode--inferior-php-process)
+  (if-let* ((php-process
+	     (get-buffer-process php-ts-mode-inferior-php-buffer)))
       (progn
 	(php-ts-mode-show-process-buffer)
-	(comint-send-string php-ts-mode--inferior-php-process "\n")
+	(comint-send-string php-process "\n")
 	(comint-send-string
-	 php-ts-mode--inferior-php-process
+	 php-process
 	 (buffer-substring-no-properties beg end))
-	(comint-send-string php-ts-mode--inferior-php-process "\n"))
+	(comint-send-string php-process "\n"))
     (message "Invoke run-php first!")))
 
 (defun php-ts-mode-send-buffer ()
-- 
2.51.0