Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
18 changes: 18 additions & 0 deletions Zend/tests/first_class_callable/constexpr/trait_scope_001.phpt
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions Zend/tests/first_class_callable/constexpr/trait_scope_002.phpt
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions Zend/tests/first_class_callable/constexpr/trait_scope_003.phpt
Original file line number Diff line number Diff line change
@@ -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
22 changes: 22 additions & 0 deletions Zend/tests/first_class_callable/constexpr/trait_scope_004.phpt
Original file line number Diff line number Diff line change
@@ -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()
22 changes: 22 additions & 0 deletions Zend/tests/first_class_callable/constexpr/trait_scope_005.phpt
Original file line number Diff line number Diff line change
@@ -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(); }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a case for the branch that still caches across scopes? A trait with a named public-method default (A::who(...)) used by two classes, both returning A. 005 covers named+private; the named+public cached path isn't exercised.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. test case added.

}
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
20 changes: 20 additions & 0 deletions Zend/tests/first_class_callable/constexpr/trait_scope_006.phpt
Original file line number Diff line number Diff line change
@@ -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
6 changes: 5 additions & 1 deletion Zend/zend_ast.c
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also disables caching for self::/parent:: and non-public FCC in non-trait default args, where the AST isn't shared and the scope is fixed, so those re-resolve the method on every default-arg evaluation. Correct as a simple fix, but worth a sentence in the description: narrowing it to only shared (trait) ASTs isn't cheap to detect at this point.

&& (fptr->common.fn_flags & ZEND_ACC_PUBLIC)) {
ZEND_MAP_PTR_SET(fcc_ast->fptr, fptr);
}
}

break;
Expand Down
Loading