Commit e5c6456d371 for php.net

commit e5c6456d3716ba8940dea34c181febea06ea9b27
Author: Daniel Scherzer <daniel.e.scherzer@gmail.com>
Date:   Mon Nov 3 14:36:57 2025 -0800

    Fix GH-20377: emit assignment for all final promoted properties (#20378)

    Previously, the assignment op line was only emitted when one of the other flags
    allowed for promoted properties (visibility, set visibility, or readonly) was
    also used, or when the property had hooks. The property was still added to the
    class, but the magical assignment `$this->prop = $prop` was missing. Add that
    assignment even when no visibility is explicitly specified, and a test to
    confirm the fix.

diff --git a/NEWS b/NEWS
index 2ebd0e05067..d807595f6ff 100644
--- a/NEWS
+++ b/NEWS
@@ -8,6 +8,8 @@ PHP                                                                        NEWS
     (ilutov)
   . Fixed bug GH-20194 (null offset deprecation not emitted for writes).
     (Girgias)
+  . Fixed bug GH-GH-20377 (final promoted properties without explicit visibility
+    not automatically assigned). (DanielEScherzer)

 - Opcache:
   . Fixed bug GH-20012 (heap buffer overflow in jit). (Arnaud)
diff --git a/Zend/tests/ctor_promotion/ctor_promotion_final.phpt b/Zend/tests/ctor_promotion/ctor_promotion_final.phpt
new file mode 100644
index 00000000000..abfb5b70880
--- /dev/null
+++ b/Zend/tests/ctor_promotion/ctor_promotion_final.phpt
@@ -0,0 +1,23 @@
+--TEST--
+GH-20377: Constructor promotion with a final property without visibility set
+--FILE--
+<?php
+
+class Demo {
+    public function __construct(
+        final string $foo,
+        final public string $bar,
+    ) {}
+}
+
+$d = new Demo("first", "second");
+var_dump($d);
+
+?>
+--EXPECTF--
+object(Demo)#%d (2) {
+  ["foo"]=>
+  string(5) "first"
+  ["bar"]=>
+  string(6) "second"
+}
diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index 0180e6e8e1c..8be1ee14f48 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -7770,6 +7770,7 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32
 		}
 	}

+	const uint32_t promotion_flags = ZEND_ACC_PPP_MASK | ZEND_ACC_PPP_SET_MASK | ZEND_ACC_READONLY | ZEND_ACC_FINAL;
 	for (i = 0; i < list->children; ++i) {
 		zend_ast *param_ast = list->child[i];
 		zend_ast *type_ast = param_ast->child[0];
@@ -7781,7 +7782,7 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32
 		zend_string *name = zval_make_interned_string(zend_ast_get_zval(var_ast));
 		bool is_ref = (param_ast->attr & ZEND_PARAM_REF) != 0;
 		bool is_variadic = (param_ast->attr & ZEND_PARAM_VARIADIC) != 0;
-		uint32_t property_flags = param_ast->attr & (ZEND_ACC_PPP_MASK | ZEND_ACC_PPP_SET_MASK | ZEND_ACC_READONLY | ZEND_ACC_FINAL);
+		uint32_t property_flags = param_ast->attr & promotion_flags;
 		bool is_promoted = property_flags || hooks_ast;

 		CG(zend_lineno) = param_ast->lineno;
@@ -8008,7 +8009,7 @@ static void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast, uint32
 		zend_ast *param_ast = list->child[i];
 		zend_ast *hooks_ast = param_ast->child[5];
 		bool is_ref = (param_ast->attr & ZEND_PARAM_REF) != 0;
-		uint32_t flags = param_ast->attr & (ZEND_ACC_PPP_MASK | ZEND_ACC_PPP_SET_MASK | ZEND_ACC_READONLY);
+		uint32_t flags = param_ast->attr & promotion_flags;
 		bool is_promoted = flags || hooks_ast;
 		if (!is_promoted) {
 			continue;