[php-src] master: zend_ast: Wrap class names in parens during export when they are an expression (#22389)

Tim Düsterhus via GitHub <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Tim Düsterhus (TimWolla)
Committer: GitHub (web-flow)
Pusher: TimWolla
Date: 2026-07-06T22:50:58+02:00

Commit: https://github.com/php/php-src/commit/7330366438084f60cfe857d92f98fa02f98ebc1e
Raw diff: https://github.com/php/php-src/commit/7330366438084f60cfe857d92f98fa02f98ebc1e.diff

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

Fixes php/php-src#22387.

Changed paths:
  A  ext/standard/tests/assert/gh22387.phpt
  M  NEWS
  M  Zend/zend_ast.c
  M  Zend/zend_ast.h
  M  Zend/zend_compile.c


Diff:

diff --git a/NEWS b/NEWS
index 2a266243a747..705ff85b2a6e 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 d3ce419c737e..3aa04de860a1 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 24b77d7d3493..86a68c1cbaf9 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 b73e2009075f..ac5a9d71a6ea 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 000000000000..38c93921608a
--- /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')
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.