Commit 73303664380 for php.net

commit 7330366438084f60cfe857d92f98fa02f98ebc1e
Author: Tim Düsterhus <tim@bastelstu.be>
Date:   Mon Jul 6 22:50:58 2026 +0200

    zend_ast: Wrap class names in parens during export when they are an expression (#22389)

    Fixes php/php-src#22387.

diff --git a/NEWS b/NEWS
index 2a266243a74..705ff85b2a6 100644
--- a/NEWS
+++ b/NEWS
@@ -4,6 +4,8 @@ PHP                                                                        NEWS

 - Core:
   . Sync Boost.Context assembly with 1.91.0. (kn1g78)
+  . Fixed bug GH-22387 (AST pretty-printing drops meaningful parentheses around
+    RHS of instanceof). (timwolla)

 - DBA:
   . Fixed OOB read on malformed length field in dba flatfile handler. (alhudz)
diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c
index d3ce419c737..3aa04de860a 100644
--- a/Zend/zend_ast.c
+++ b/Zend/zend_ast.c
@@ -1686,6 +1686,19 @@ static ZEND_COLD void zend_ast_export_ns_name(smart_str *str, zend_ast *ast, int
 	}
 	zend_ast_export_ex(str, ast, priority, indent);
 }
+static ZEND_COLD void zend_ast_export_ns_name_or_expression(smart_str *str, zend_ast *ast, int priority, int indent)
+{
+	switch (ast->kind) {
+		case ZEND_AST_ZVAL:
+		case ZEND_AST_VAR:
+			zend_ast_export_ns_name(str, ast, priority, indent);
+			break;
+		default:
+			smart_str_appendc(str, '(');
+			zend_ast_export_ex(str, ast, priority, indent);
+			smart_str_appendc(str, ')');
+	}
+}

 static ZEND_COLD bool zend_ast_valid_var_name(const char *s, size_t len)
 {
@@ -2523,25 +2536,12 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio
 			zend_ast_export_var(str, ast->child[1], indent);
 			break;
 		case ZEND_AST_STATIC_PROP:
-			zend_ast_export_ns_name(str, ast->child[0], 0, indent);
+			zend_ast_export_ns_name_or_expression(str, ast->child[0], 0, indent);
 			smart_str_appends(str, "::$");
 			zend_ast_export_var(str, ast->child[1], indent);
 			break;
 		case ZEND_AST_CALL: {
-			zend_ast *left = ast->child[0];
-			switch (left->kind) {
-				/* ZEND_AST_ZVAL is a regular function call. */
-				case ZEND_AST_ZVAL:
-				/* ZEND_AST_VAR ($foo()) is unambiguous without parens. */
-				case ZEND_AST_VAR:
-					zend_ast_export_ns_name(str, left, 0, indent);
-					break;
-				default:
-					smart_str_appendc(str, '(');
-					zend_ast_export_ex(str, left, 0, indent);
-					smart_str_appendc(str, ')');
-					break;
-			}
+			zend_ast_export_ns_name_or_expression(str, ast->child[0], 0, indent);
 			smart_str_appendc(str, '(');
 			zend_ast_export_ex(str, ast->child[1], 0, indent);
 			smart_str_appendc(str, ')');
@@ -2553,7 +2553,7 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio
 			goto simple_list;
 		}
 		case ZEND_AST_CLASS_CONST:
-			zend_ast_export_ns_name(str, ast->child[0], 0, indent);
+			zend_ast_export_ns_name_or_expression(str, ast->child[0], 0, indent);
 			smart_str_appends(str, "::");
 			zend_ast_export_name(str, ast->child[1], 0, indent);
 			break;
@@ -2570,7 +2570,7 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio
 					default: ZEND_UNREACHABLE();
 				}
 			} else {
-				zend_ast_export_ns_name(str, ast->child[0], 0, indent);
+				zend_ast_export_ns_name_or_expression(str, ast->child[0], 0, indent);
 			}
 			smart_str_appends(str, "::class");
 			break;
@@ -2649,7 +2649,7 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio
 				}
 				zend_ast_export_class_no_header(str, decl, indent);
 			} else {
-				zend_ast_export_ns_name(str, ast->child[0], 0, indent);
+				zend_ast_export_ns_name_or_expression(str, ast->child[0], 0, indent);
 				smart_str_appendc(str, '(');
 				zend_ast_export_ex(str, ast->child[1], 0, indent);
 				smart_str_appendc(str, ')');
@@ -2658,7 +2658,7 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio
 		case ZEND_AST_INSTANCEOF:
 			zend_ast_export_ex(str, ast->child[0], 0, indent);
 			smart_str_appends(str, " instanceof ");
-			zend_ast_export_ns_name(str, ast->child[1], 0, indent);
+			zend_ast_export_ns_name_or_expression(str, ast->child[1], 0, indent);
 			break;
 		case ZEND_AST_YIELD:
 			if (priority > 70) smart_str_appendc(str, '(');
@@ -2851,7 +2851,12 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio
 			smart_str_appendc(str, ')');
 			break;
 		case ZEND_AST_STATIC_CALL:
-			zend_ast_export_ns_name(str, ast->child[0], 0, indent);
+			if (zend_ast_is_parent_hook_call(ast)) {
+				zend_ast_export_ns_name(str, ast->child[0], 0, indent);
+			} else {
+				zend_ast_export_ns_name_or_expression(str, ast->child[0], 0, indent);
+			}
+
 			smart_str_appends(str, "::");
 			zend_ast_export_var(str, ast->child[1], indent);
 			smart_str_appendc(str, '(');
@@ -3082,3 +3087,22 @@ zend_ast * ZEND_FASTCALL zend_ast_call_get_args(zend_ast *ast)
 	ZEND_UNREACHABLE();
 	return NULL;
 }
+
+bool zend_ast_is_parent_hook_call(const zend_ast *ast)
+{
+	ZEND_ASSERT(ast->kind == ZEND_AST_STATIC_CALL);
+
+	const zend_ast *class_ast = ast->child[0];
+	zend_ast *method_ast = ast->child[1];
+
+	return class_ast->kind == ZEND_AST_STATIC_PROP
+		&& !(class_ast->attr & ZEND_PARENTHESIZED_STATIC_PROP)
+		&& class_ast->child[0]->kind == ZEND_AST_ZVAL
+		&& Z_TYPE_P(zend_ast_get_zval(class_ast->child[0])) == IS_STRING
+		&& zend_get_class_fetch_type(zend_ast_get_str(class_ast->child[0])) == ZEND_FETCH_CLASS_PARENT
+		&& class_ast->child[1]->kind == ZEND_AST_ZVAL
+		&& method_ast->kind == ZEND_AST_ZVAL
+		&& Z_TYPE_P(zend_ast_get_zval(method_ast)) == IS_STRING
+		&& (zend_string_equals_literal_ci(zend_ast_get_str(method_ast), "get")
+			|| zend_string_equals_literal_ci(zend_ast_get_str(method_ast), "set"));
+}
diff --git a/Zend/zend_ast.h b/Zend/zend_ast.h
index 24b77d7d349..86a68c1cbaf 100644
--- a/Zend/zend_ast.h
+++ b/Zend/zend_ast.h
@@ -440,4 +440,7 @@ zend_ast * ZEND_FASTCALL zend_ast_with_attributes(zend_ast *ast, zend_ast *attr)

 zend_ast * ZEND_FASTCALL zend_ast_call_get_args(zend_ast *ast);

+/* Recognize parent::$prop::get() pattern. */
+bool zend_ast_is_parent_hook_call(const zend_ast *ast);
+
 #endif
diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index b73e2009075..ac5a9d71a6e 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -5353,17 +5353,7 @@ static bool zend_compile_parent_property_hook_call(znode *result, const zend_ast
 	const zend_ast *class_ast = ast->child[0];
 	zend_ast *method_ast = ast->child[1];

-	/* Recognize parent::$prop::get() pattern. */
-	if (class_ast->kind != ZEND_AST_STATIC_PROP
-	 || (class_ast->attr & ZEND_PARENTHESIZED_STATIC_PROP)
-	 || class_ast->child[0]->kind != ZEND_AST_ZVAL
-	 || Z_TYPE_P(zend_ast_get_zval(class_ast->child[0])) != IS_STRING
-	 || zend_get_class_fetch_type(zend_ast_get_str(class_ast->child[0])) != ZEND_FETCH_CLASS_PARENT
-	 || class_ast->child[1]->kind != ZEND_AST_ZVAL
-	 || method_ast->kind != ZEND_AST_ZVAL
-	 || Z_TYPE_P(zend_ast_get_zval(method_ast)) != IS_STRING
-	 || (!zend_string_equals_literal_ci(zend_ast_get_str(method_ast), "get")
-	  && !zend_string_equals_literal_ci(zend_ast_get_str(method_ast), "set"))) {
+	if (!zend_ast_is_parent_hook_call(ast)) {
 		return false;
 	}

diff --git a/ext/standard/tests/assert/gh22387.phpt b/ext/standard/tests/assert/gh22387.phpt
new file mode 100644
index 00000000000..38c93921608
--- /dev/null
+++ b/ext/standard/tests/assert/gh22387.phpt
@@ -0,0 +1,62 @@
+--TEST--
+GH-22387: AST pretty-printing drops meaningful parentheses around RHS of instanceof
+--FILE--
+<?php
+
+class Foo {
+	public static $p = true;
+	public const C = true;
+
+	public static function m() {
+		return true;
+	}
+}
+
+$foo = new Foo();
+const bar = 'Foo';
+const baz = new stdClass();
+
+try {
+	assert(!$foo instanceof (bar));
+} catch (AssertionError $e) {
+	echo $e->getMessage(), PHP_EOL;
+}
+
+try {
+	assert(!new (bar)());
+} catch (AssertionError $e) {
+	echo $e->getMessage(), PHP_EOL;
+}
+
+try {
+	assert(!(bar)::m());
+} catch (AssertionError $e) {
+	echo $e->getMessage(), PHP_EOL;
+}
+
+try {
+	assert(!(bar)::$p);
+} catch (AssertionError $e) {
+	echo $e->getMessage(), PHP_EOL;
+}
+
+try {
+	assert(!(bar)::C);
+} catch (AssertionError $e) {
+	echo $e->getMessage(), PHP_EOL;
+}
+
+try {
+	assert((baz)::class !== 'stdClass');
+} catch (AssertionError $e) {
+	echo $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+assert(!$foo instanceof (bar))
+assert(!new (bar)())
+assert(!(bar)::m())
+assert(!(bar)::$p)
+assert(!(bar)::C)
+assert((baz)::class !== 'stdClass')