Commit 44a2a2c3db9 for php

commit 44a2a2c3db9e4e06c41a0603c3826fac3b1b99f3
Author: Jorg Sowa <jorg.sowa@gmail.com>
Date:   Tue Nov 3 11:07:28 2026 +0100

    Fix FCC in trait const expressions resolving to the first using class

    The resolved method was cached on the shared AST node, so self::/parent::
    and visibility were only checked for the first class evaluating it.

    Closes GH-25083

diff --git a/NEWS b/NEWS
index 5de475a2252..8fcac3a57a5 100644
--- a/NEWS
+++ b/NEWS
@@ -37,6 +37,9 @@ PHP                                                                        NEWS
     (Jorg Sowa)
   . Fixed bug GH-25061 ($x = $x - 1 on null is optimized to PRE_DEC).
     (Lazizbek Ergashev)
+  . Fixed first-class callables to static methods in trait constant
+    expressions resolving to the method of the first using class and
+    bypassing visibility checks. (Jorg Sowa)

 - CLI:
   . Fixed bug GH-24697 (PHP_HTTP_PARSER_STRICT build option doesn't work).
diff --git a/Zend/tests/first_class_callable/constexpr/trait_scope_001.phpt b/Zend/tests/first_class_callable/constexpr/trait_scope_001.phpt
new file mode 100644
index 00000000000..8199b47afa9
--- /dev/null
+++ b/Zend/tests/first_class_callable/constexpr/trait_scope_001.phpt
@@ -0,0 +1,18 @@
+--TEST--
+FCC for self:: static method in trait default argument is resolved per using class
+--FILE--
+<?php
+
+trait T {
+    public static function make($f = self::who(...)) { return $f(); }
+}
+class A { use T; public static function who() { return 'A'; } }
+class B { use T; public static function who() { return 'B'; } }
+
+echo A::make(), PHP_EOL;
+echo B::make(), PHP_EOL;
+
+?>
+--EXPECT--
+A
+B
diff --git a/Zend/tests/first_class_callable/constexpr/trait_scope_002.phpt b/Zend/tests/first_class_callable/constexpr/trait_scope_002.phpt
new file mode 100644
index 00000000000..5390f6337a5
--- /dev/null
+++ b/Zend/tests/first_class_callable/constexpr/trait_scope_002.phpt
@@ -0,0 +1,20 @@
+--TEST--
+FCC for parent:: static method in trait default argument is resolved per using class
+--FILE--
+<?php
+
+class P1 { public static function who() { return 'P1'; } }
+class P2 { public static function who() { return 'P2'; } }
+trait T {
+    public static function make($f = parent::who(...)) { return $f(); }
+}
+class A extends P1 { use T; }
+class B extends P2 { use T; }
+
+echo A::make(), PHP_EOL;
+echo B::make(), PHP_EOL;
+
+?>
+--EXPECT--
+P1
+P2
diff --git a/Zend/tests/first_class_callable/constexpr/trait_scope_003.phpt b/Zend/tests/first_class_callable/constexpr/trait_scope_003.phpt
new file mode 100644
index 00000000000..ae63777392b
--- /dev/null
+++ b/Zend/tests/first_class_callable/constexpr/trait_scope_003.phpt
@@ -0,0 +1,23 @@
+--TEST--
+FCC for self:: static method in trait constant and property default is resolved per using class
+--FILE--
+<?php
+
+trait T {
+    const C = self::who(...);
+    public $p = self::who(...);
+}
+class A { use T; public static function who() { return 'A'; } }
+class B { use T; public static function who() { return 'B'; } }
+
+echo (A::C)(), PHP_EOL;
+echo (B::C)(), PHP_EOL;
+echo ((new A)->p)(), PHP_EOL;
+echo ((new B)->p)(), PHP_EOL;
+
+?>
+--EXPECT--
+A
+B
+A
+B
diff --git a/Zend/tests/first_class_callable/constexpr/trait_scope_004.phpt b/Zend/tests/first_class_callable/constexpr/trait_scope_004.phpt
new file mode 100644
index 00000000000..da1608e2d91
--- /dev/null
+++ b/Zend/tests/first_class_callable/constexpr/trait_scope_004.phpt
@@ -0,0 +1,22 @@
+--TEST--
+FCC for self:: private static method in trait default argument does not leak to other using classes
+--FILE--
+<?php
+
+trait T {
+    public static function make($f = self::who(...)) { return $f(); }
+}
+class A { use T; private static function who() { return 'A'; } }
+class B { use T; }
+
+echo A::make(), PHP_EOL;
+try {
+    B::make();
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+A
+Error: Call to undefined method B::who()
diff --git a/Zend/tests/first_class_callable/constexpr/trait_scope_005.phpt b/Zend/tests/first_class_callable/constexpr/trait_scope_005.phpt
new file mode 100644
index 00000000000..50382a44993
--- /dev/null
+++ b/Zend/tests/first_class_callable/constexpr/trait_scope_005.phpt
@@ -0,0 +1,22 @@
+--TEST--
+FCC for named class private static method in trait default argument checks visibility per using class
+--FILE--
+<?php
+
+trait T {
+    public static function make($f = A::who(...)) { return $f(); }
+}
+class A { use T; private static function who() { return 'A'; } }
+class B { use T; }
+
+echo A::make(), PHP_EOL;
+try {
+    B::make();
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+A
+Error: Call to private method A::who() from scope B
diff --git a/Zend/tests/first_class_callable/constexpr/trait_scope_006.phpt b/Zend/tests/first_class_callable/constexpr/trait_scope_006.phpt
new file mode 100644
index 00000000000..4ffb56841ca
--- /dev/null
+++ b/Zend/tests/first_class_callable/constexpr/trait_scope_006.phpt
@@ -0,0 +1,20 @@
+--TEST--
+FCC for named class public static method in trait default argument is shared across using classes
+--FILE--
+<?php
+
+trait T {
+    public static function make($f = A::who(...)) { return $f(); }
+}
+class A { use T; public static function who() { return 'A'; } }
+class B { use T; }
+
+echo A::make(), PHP_EOL;
+echo B::make(), PHP_EOL;
+echo A::make(), PHP_EOL;
+
+?>
+--EXPECT--
+A
+A
+A
diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c
index 791bc8c1f23..f7eb7ee05e6 100644
--- a/Zend/zend_ast.c
+++ b/Zend/zend_ast.c
@@ -1148,7 +1148,11 @@ ZEND_API zend_result ZEND_FASTCALL zend_ast_evaluate_inner(
 							}
 						}

-						ZEND_MAP_PTR_SET(fcc_ast->fptr, fptr);
+						/* The AST may be shared between scopes (e.g. via traits), so only cache scope-independent results. */
+						if ((ast->child[0]->attr >> ZEND_CONST_EXPR_NEW_FETCH_TYPE_SHIFT) == ZEND_FETCH_CLASS_DEFAULT
+						 && (fptr->common.fn_flags & ZEND_ACC_PUBLIC)) {
+							ZEND_MAP_PTR_SET(fcc_ast->fptr, fptr);
+						}
 					}

 					break;