Commit 29665a6f04a for php

commit 29665a6f04a5bd8882a26e7eaa6327f29b5d7ae4
Author: Jorg Sowa <jorg.sowa@gmail.com>
Date:   Thu Nov 5 22:06:01 2026 +0100

    Fix AST export dropping braces of dynamic class constant fetches

    Closes GH-25221

diff --git a/NEWS b/NEWS
index 6fde7abcbc4..3c5ffd7802b 100644
--- a/NEWS
+++ b/NEWS
@@ -6,6 +6,8 @@ PHP                                                                        NEWS
   . Fixed AST printing of interface extends list as implements. (Jorg Sowa)
   . Fixed AST printing dropping fully qualified and namespace-relative prefixes
     of class names in implements, catch, and trait use lists. (Jorg Sowa)
+  . Fixed AST export dropping the braces of dynamic class constant fetches.
+    (Jorg Sowa)

 - Curl:
   . Fixed curl_reset() retaining CURLOPT_PRIVATE after reset. (lacatoire)
diff --git a/Zend/tests/assert/ast_export_dynamic_class_const.phpt b/Zend/tests/assert/ast_export_dynamic_class_const.phpt
new file mode 100644
index 00000000000..43a5e31c714
--- /dev/null
+++ b/Zend/tests/assert/ast_export_dynamic_class_const.phpt
@@ -0,0 +1,50 @@
+--TEST--
+AST export of dynamic class constant fetches
+--INI--
+zend.assertions=1
+--FILE--
+<?php
+
+$tests = [
+    fn() => assert(false && C::FOO),
+    fn() => assert(false && C::list),
+    fn() => assert(false && C::class),
+    fn() => assert(false && C::{$x}),
+    fn() => assert(false && C::{FOO}),
+    fn() => assert(false && C::{foo()}),
+    fn() => assert(false && C::{$a . $b}),
+    fn() => assert(false && C::{1}),
+    fn() => assert(false && C::{'foo'}),
+    fn() => assert(false && C::{'a b'}),
+    fn() => assert(false && C::{''}),
+    fn() => assert(false && C::{'class'}),
+    fn() => assert(false && C::{'CLASS'}),
+    fn() => assert(false && $o::{$x}),
+    fn() => assert(false && static::{$x}),
+];
+
+foreach ($tests as $test) {
+    try {
+        $test();
+    } catch (AssertionError $e) {
+        echo $e::class, ': ', $e->getMessage(), "\n";
+    }
+}
+
+?>
+--EXPECT--
+AssertionError: assert(false && C::FOO)
+AssertionError: assert(false && C::list)
+AssertionError: assert(false && C::class)
+AssertionError: assert(false && C::{$x})
+AssertionError: assert(false && C::{FOO})
+AssertionError: assert(false && C::{foo()})
+AssertionError: assert(false && C::{$a . $b})
+AssertionError: assert(false && C::{1})
+AssertionError: assert(false && C::foo)
+AssertionError: assert(false && C::{'a b'})
+AssertionError: assert(false && C::{''})
+AssertionError: assert(false && C::{'class'})
+AssertionError: assert(false && C::{'CLASS'})
+AssertionError: assert(false && $o::{$x})
+AssertionError: assert(false && static::{$x})
diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c
index dc40c15fc45..7d6038a194d 100644
--- a/Zend/zend_ast.c
+++ b/Zend/zend_ast.c
@@ -1429,6 +1429,24 @@ static ZEND_COLD bool zend_ast_var_needs_braces(char ch)
 	return ch == '[' || zend_ast_valid_var_char(ch);
 }

+static ZEND_COLD void zend_ast_export_class_const_name(smart_str *str, zend_ast *ast, int indent)
+{
+	if (ast->kind == ZEND_AST_ZVAL) {
+		const zval *zv = zend_ast_get_zval(ast);
+
+		/* A bare "class" would turn the dynamic fetch into a class name fetch. */
+		if (Z_TYPE_P(zv) == IS_STRING
+				&& zend_ast_valid_var_name(Z_STRVAL_P(zv), Z_STRLEN_P(zv))
+				&& !zend_string_equals_ci(Z_STR_P(zv), ZSTR_KNOWN(ZEND_STR_CLASS))) {
+			smart_str_append(str, Z_STR_P(zv));
+			return;
+		}
+	}
+	smart_str_appendc(str, '{');
+	zend_ast_export_ex(str, ast, 0, indent);
+	smart_str_appendc(str, '}');
+}
+
 static ZEND_COLD void zend_ast_export_var(smart_str *str, zend_ast *ast, int priority, int indent)
 {
 	if (ast->kind == ZEND_AST_ZVAL) {
@@ -2237,7 +2255,7 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio
 		case ZEND_AST_CLASS_CONST:
 			zend_ast_export_ns_name(str, ast->child[0], 0, indent);
 			smart_str_appends(str, "::");
-			zend_ast_export_name(str, ast->child[1], 0, indent);
+			zend_ast_export_class_const_name(str, ast->child[1], indent);
 			break;
 		case ZEND_AST_CLASS_NAME:
 			if (ast->child[0] == NULL) {