1
0
mirror of https://github.com/php/php-src.git synced 2026-03-24 00:02:20 +01:00

[RFC] Add #[\DelayedTargetValidation] attribute (#18817)

https://wiki.php.net/rfc/delayedtargetvalidation_attribute
This commit is contained in:
Daniel Scherzer
2025-08-20 00:41:20 -07:00
committed by GitHub
parent 1cff1815d0
commit 63acc4bf61
32 changed files with 1769 additions and 66 deletions

View File

@@ -53,6 +53,8 @@ PHP 8.5 UPGRADE NOTES
. Applying #[\Attribute] to an abstract class, enum, interface, or trait triggers
an error during compilation. Previously, the attribute could be added, but when
ReflectionAttribute::newInstance() was called an error would be thrown.
The error can be delayed from compilation to runtime using the new
#[\DelayedTargetValidation] attribute.
- DOM:
. Cloning a DOMNamedNodeMap, DOMNodeList, Dom\NamedNodeMap, Dom\NodeList,
@@ -184,6 +186,11 @@ PHP 8.5 UPGRADE NOTES
RFC: https://wiki.php.net/rfc/final_promotion
. #[\Override] can now be applied to properties.
RFC: https://wiki.php.net/rfc/override_properties
. The #[\DelayedTargetValidation] attribute can be used to suppress
compile-time errors from core (or extension) attributes that are used on
invalid targets. These errors are instead reported at runtime if and when
ReflectionAttribute::newInstance() is called.
RFC: https://wiki.php.net/rfc/delayedtargetvalidation_attribute
- Curl:
. Added support for share handles that are persisted across multiple PHP
@@ -528,6 +535,11 @@ PHP 8.5 UPGRADE NOTES
hooks are final, and whether the property is virtual. This also affects
the output of ReflectionClass::__toString() when a class contains hooked
properties.
. ReflectionAttribute::newInstance() can now throw errors for internal
attributes if the attribute was applied on an invalid target and the
error was delayed from compile-time to runtime via the
#[\DelayedTargetValidation] attribute.
RFC: https://wiki.php.net/rfc/delayedtargetvalidation_attribute
- Session:
. session_start is stricter in regard to the option argument.
@@ -648,6 +660,8 @@ PHP 8.5 UPGRADE NOTES
- Core:
. NoDiscard attribute was added.
RFC: https://wiki.php.net/rfc/marking_return_value_as_important
. DelayedTargetValidation attribute was added.
RFC: https://wiki.php.net/rfc/delayedtargetvalidation_attribute
- Curl:
. CurlSharePersistentHandle representing a share handle that is persisted

View File

@@ -28,6 +28,12 @@ PHP 8.5 INTERNALS UPGRADE NOTES
extra layer of indirection can be removed. In other cases a zval can
be heap-allocated and stored in the pointer as a minimal change to keep
compatibility.
. The validator callbacks for internal attribute now return `zend_string *`
rather than `void`; instead of emitting an error when an attribute is
applied incorrectly, the error message should be returned as a zend_string
pointer. If the error will be delayed until runtime, it is stored in the
new `validation_error` field of the `zend_attribute` struct.
RFC: https://wiki.php.net/rfc/delayedtargetvalidation_attribute
- Hash
. Hash functions now use proper hash_spec_result enum for return values

View File

@@ -0,0 +1,282 @@
--TEST--
#[\DelayedTargetValidation] has errors at runtime
--FILE--
<?php
#[DelayedTargetValidation]
#[NoDiscard]
class Demo {
#[DelayedTargetValidation]
#[Attribute]
public const FOO = 'BAR';
#[DelayedTargetValidation]
#[Attribute]
public string $v1;
public string $v2 {
#[DelayedTargetValidation]
#[Attribute]
get => $this->v2;
#[DelayedTargetValidation]
#[Attribute]
set => $value;
}
#[DelayedTargetValidation]
#[Attribute]
public function __construct(
#[DelayedTargetValidation]
#[Attribute]
public string $v3
) {
$this->v1 = $v3;
echo __METHOD__ . "\n";
}
}
#[DelayedTargetValidation]
#[Attribute]
function demoFn() {
echo __FUNCTION__ . "\n";
}
#[DelayedTargetValidation]
#[Attribute]
const EXAMPLE = true;
$cases = [
new ReflectionClass('Demo'),
new ReflectionClassConstant('Demo', 'FOO'),
new ReflectionProperty('Demo', 'v1'),
new ReflectionProperty('Demo', 'v2')->getHook(PropertyHookType::Get),
new ReflectionProperty('Demo', 'v2')->getHook(PropertyHookType::Set),
new ReflectionMethod('Demo', '__construct'),
new ReflectionParameter([ 'Demo', '__construct' ], 'v3'),
new ReflectionProperty('Demo', 'v3'),
new ReflectionFunction('demoFn'),
new ReflectionConstant('EXAMPLE'),
];
foreach ($cases as $r) {
echo str_repeat("*", 20) . "\n";
echo $r . "\n";
$attributes = $r->getAttributes();
var_dump($attributes);
try {
$attributes[1]->newInstance();
} catch (Error $e) {
echo get_class($e) . ": " . $e->getMessage() . "\n";
}
}
?>
--EXPECTF--
********************
Class [ <user> <iterateable> class Demo ] {
@@ %s %d-%d
- Constants [1] {
Constant [ public string FOO ] { BAR }
}
- Static properties [0] {
}
- Static methods [0] {
}
- Properties [3] {
Property [ public string $v1 ]
Property [ public string $v2 { get; set; } ]
Property [ public string $v3 ]
}
- Methods [1] {
Method [ <user, ctor> public method __construct ] {
@@ %s %d - %d
- Parameters [1] {
Parameter #0 [ <required> string $v3 ]
}
}
}
}
array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "NoDiscard"
}
}
Error: Attribute "NoDiscard" cannot target class (allowed targets: function, method)
********************
Constant [ public string FOO ] { BAR }
array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "Attribute"
}
}
Error: Attribute "Attribute" cannot target class constant (allowed targets: class)
********************
Property [ public string $v1 ]
array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "Attribute"
}
}
Error: Attribute "Attribute" cannot target property (allowed targets: class)
********************
Method [ <user> public method $v2::get ] {
@@ %s %d - %d
- Parameters [0] {
}
- Return [ string ]
}
array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "Attribute"
}
}
Error: Attribute "Attribute" cannot target method (allowed targets: class)
********************
Method [ <user> public method $v2::set ] {
@@ %s %d - %d
- Parameters [1] {
Parameter #0 [ <required> string $value ]
}
- Return [ void ]
}
array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "Attribute"
}
}
Error: Attribute "Attribute" cannot target method (allowed targets: class)
********************
Method [ <user, ctor> public method __construct ] {
@@ %s %d - %d
- Parameters [1] {
Parameter #0 [ <required> string $v3 ]
}
}
array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "Attribute"
}
}
Error: Attribute "Attribute" cannot target method (allowed targets: class)
********************
Parameter #0 [ <required> string $v3 ]
array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "Attribute"
}
}
Error: Attribute "Attribute" cannot target parameter (allowed targets: class)
********************
Property [ public string $v3 ]
array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "Attribute"
}
}
Error: Attribute "Attribute" cannot target property (allowed targets: class)
********************
Function [ <user> function demoFn ] {
@@ %s %d - %d
}
array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "Attribute"
}
}
Error: Attribute "Attribute" cannot target function (allowed targets: class)
********************
Constant [ bool EXAMPLE ] { 1 }
array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "Attribute"
}
}
Error: Attribute "Attribute" cannot target constant (allowed targets: class)

View File

@@ -0,0 +1,55 @@
--TEST--
#[\DelayedTargetValidation] prevents target errors at compile time
--FILE--
<?php
#[DelayedTargetValidation]
#[NoDiscard]
class Demo {
#[DelayedTargetValidation]
#[Attribute]
public const FOO = 'BAR';
#[DelayedTargetValidation]
#[Attribute]
public string $v1;
public string $v2 {
#[DelayedTargetValidation]
#[Attribute]
get => $this->v2;
#[DelayedTargetValidation]
#[Attribute]
set => $value;
}
#[DelayedTargetValidation]
#[Attribute]
public function __construct(
#[DelayedTargetValidation]
#[Attribute]
public string $v3
) {
$this->v1 = $v3;
echo __METHOD__ . "\n";
}
}
#[DelayedTargetValidation]
#[Attribute]
function demoFn() {
echo __FUNCTION__ . "\n";
}
$o = new Demo( "foo" );
demoFn();
#[DelayedTargetValidation]
#[Attribute]
const EXAMPLE = true;
?>
--EXPECT--
Demo::__construct
demoFn

View File

@@ -0,0 +1,5 @@
<?php
#[DelayedTargetValidation]
#[AllowDynamicProperties]
trait DemoTrait {}

View File

@@ -0,0 +1,58 @@
--TEST--
#[\DelayedTargetValidation] with preloaded attribute with errors
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.preload={PWD}/opcache_validator_errors.inc
--EXTENSIONS--
opcache
--SKIPIF--
<?php
if (PHP_OS_FAMILY == 'Windows') die('skip Preloading is not supported on Windows');
?>
--FILE--
<?php
$r = new ReflectionClass('DemoTrait');
echo $r . "\n";
$attributes = $r->getAttributes();
var_dump($attributes);
try {
$attributes[1]->newInstance();
} catch (Error $e) {
echo get_class($e) . ": " . $e->getMessage() . "\n";
}
?>
--EXPECTF--
Trait [ <user> trait DemoTrait ] {
@@ %s %d-%d
- Constants [0] {
}
- Static properties [0] {
}
- Static methods [0] {
}
- Properties [0] {
}
- Methods [0] {
}
}
array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(22) "AllowDynamicProperties"
}
}
Error: Cannot apply #[\AllowDynamicProperties] to trait DemoTrait

View File

@@ -0,0 +1,13 @@
--TEST--
#[\DelayedTargetValidation] does not prevent repetition errors
--FILE--
<?php
#[DelayedTargetValidation]
#[NoDiscard]
#[NoDiscard]
class Demo {}
?>
--EXPECTF--
Fatal error: Attribute "NoDiscard" must not be repeated in %s on line %d

View File

@@ -0,0 +1,180 @@
--TEST--
#[\DelayedTargetValidation] with #[\AllowDynamicProperties]: validator errors delayed
--FILE--
<?php
#[DelayedTargetValidation]
#[AllowDynamicProperties]
trait DemoTrait {}
#[DelayedTargetValidation]
#[AllowDynamicProperties]
interface DemoInterface {}
#[DelayedTargetValidation]
#[AllowDynamicProperties]
readonly class DemoReadonly {}
#[DelayedTargetValidation]
#[AllowDynamicProperties]
enum DemoEnum {}
$cases = [
new ReflectionClass('DemoTrait'),
new ReflectionClass('DemoInterface'),
new ReflectionClass('DemoReadonly'),
new ReflectionClass('DemoEnum'),
];
foreach ($cases as $r) {
echo str_repeat("*", 20) . "\n";
echo $r . "\n";
$attributes = $r->getAttributes();
var_dump($attributes);
try {
$attributes[1]->newInstance();
} catch (Error $e) {
echo get_class($e) . ": " . $e->getMessage() . "\n";
}
}
?>
--EXPECTF--
********************
Trait [ <user> trait DemoTrait ] {
@@ %s %d-%d
- Constants [0] {
}
- Static properties [0] {
}
- Static methods [0] {
}
- Properties [0] {
}
- Methods [0] {
}
}
array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(22) "AllowDynamicProperties"
}
}
Error: Cannot apply #[\AllowDynamicProperties] to trait DemoTrait
********************
Interface [ <user> interface DemoInterface ] {
@@ %s %d-%d
- Constants [0] {
}
- Static properties [0] {
}
- Static methods [0] {
}
- Properties [0] {
}
- Methods [0] {
}
}
array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(22) "AllowDynamicProperties"
}
}
Error: Cannot apply #[\AllowDynamicProperties] to interface DemoInterface
********************
Class [ <user> readonly class DemoReadonly ] {
@@ %s %d-%d
- Constants [0] {
}
- Static properties [0] {
}
- Static methods [0] {
}
- Properties [0] {
}
- Methods [0] {
}
}
array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(22) "AllowDynamicProperties"
}
}
Error: Cannot apply #[\AllowDynamicProperties] to readonly class DemoReadonly
********************
Enum [ <user> enum DemoEnum implements UnitEnum ] {
@@ %s %d-%d
- Constants [0] {
}
- Static properties [0] {
}
- Static methods [1] {
Method [ <internal, prototype UnitEnum> static public method cases ] {
- Parameters [0] {
}
- Return [ array ]
}
}
- Properties [1] {
Property [ public protected(set) readonly string $name ]
}
- Methods [0] {
}
}
array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(22) "AllowDynamicProperties"
}
}
Error: Cannot apply #[\AllowDynamicProperties] to enum DemoEnum

View File

@@ -0,0 +1,180 @@
--TEST--
#[\DelayedTargetValidation] with #[\Attribute]: validator errors delayed
--FILE--
<?php
#[DelayedTargetValidation]
#[Attribute]
trait DemoTrait {}
#[DelayedTargetValidation]
#[Attribute]
interface DemoInterface {}
#[DelayedTargetValidation]
#[Attribute]
abstract class DemoAbstract {}
#[DelayedTargetValidation]
#[Attribute]
enum DemoEnum {}
$cases = [
new ReflectionClass('DemoTrait'),
new ReflectionClass('DemoInterface'),
new ReflectionClass('DemoAbstract'),
new ReflectionClass('DemoEnum'),
];
foreach ($cases as $r) {
echo str_repeat("*", 20) . "\n";
echo $r . "\n";
$attributes = $r->getAttributes();
var_dump($attributes);
try {
$attributes[1]->newInstance();
} catch (Error $e) {
echo get_class($e) . ": " . $e->getMessage() . "\n";
}
}
?>
--EXPECTF--
********************
Trait [ <user> trait DemoTrait ] {
@@ %s %d-%d
- Constants [0] {
}
- Static properties [0] {
}
- Static methods [0] {
}
- Properties [0] {
}
- Methods [0] {
}
}
array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "Attribute"
}
}
Error: Cannot apply #[\Attribute] to trait DemoTrait
********************
Interface [ <user> interface DemoInterface ] {
@@ %s %d-%d
- Constants [0] {
}
- Static properties [0] {
}
- Static methods [0] {
}
- Properties [0] {
}
- Methods [0] {
}
}
array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "Attribute"
}
}
Error: Cannot apply #[\Attribute] to interface DemoInterface
********************
Class [ <user> abstract class DemoAbstract ] {
@@ %s %d-%d
- Constants [0] {
}
- Static properties [0] {
}
- Static methods [0] {
}
- Properties [0] {
}
- Methods [0] {
}
}
array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "Attribute"
}
}
Error: Cannot apply #[\Attribute] to abstract class DemoAbstract
********************
Enum [ <user> enum DemoEnum implements UnitEnum ] {
@@ %s %d-%d
- Constants [0] {
}
- Static properties [0] {
}
- Static methods [1] {
Method [ <internal, prototype UnitEnum> static public method cases ] {
- Parameters [0] {
}
- Return [ array ]
}
}
- Properties [1] {
Property [ public protected(set) readonly string $name ]
}
- Methods [0] {
}
}
array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "Attribute"
}
}
Error: Cannot apply #[\Attribute] to enum DemoEnum

View File

@@ -0,0 +1,79 @@
--TEST--
#[\DelayedTargetValidation] with #[\NoDiscard]: validator errors delayed
--FILE--
<?php
class DemoClass {
public string $hooked {
#[DelayedTargetValidation]
#[NoDiscard] // Does nothing here
get => $this->hooked;
#[DelayedTargetValidation]
#[NoDiscard] // Does nothing here
set => $value;
}
}
$cases = [
new ReflectionProperty('DemoClass', 'hooked')->getHook(PropertyHookType::Get),
new ReflectionProperty('DemoClass', 'hooked')->getHook(PropertyHookType::Set),
];
foreach ($cases as $r) {
echo str_repeat("*", 20) . "\n";
echo $r . "\n";
$attributes = $r->getAttributes();
var_dump($attributes);
try {
$attributes[1]->newInstance();
} catch (Error $e) {
echo get_class($e) . ": " . $e->getMessage() . "\n";
}
}
?>
--EXPECTF--
********************
Method [ <user> public method $hooked::get ] {
@@ %s %d - %d
- Parameters [0] {
}
- Return [ string ]
}
array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "NoDiscard"
}
}
Error: #[\NoDiscard] is not supported for property hooks
********************
Method [ <user> public method $hooked::set ] {
@@ %s %d - %d
- Parameters [1] {
Parameter #0 [ <required> string $value ]
}
- Return [ void ]
}
array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(9) "NoDiscard"
}
}
Error: #[\NoDiscard] is not supported for property hooks

View File

@@ -0,0 +1,62 @@
--TEST--
#[\DelayedTargetValidation] with a successful validator
--FILE--
<?php
#[DelayedTargetValidation]
#[AllowDynamicProperties]
class DemoClass {}
$obj = new DemoClass();
var_dump($obj);
// No warnings
$obj->dynamic = true;
var_dump($obj);
$ref = new ReflectionClass('DemoClass');
echo $ref . "\n";
$attributes = $ref->getAttributes();
var_dump($attributes);
var_dump($attributes[1]->newInstance());
?>
--EXPECTF--
object(DemoClass)#%d (0) {
}
object(DemoClass)#%d (1) {
["dynamic"]=>
bool(true)
}
Class [ <user> class DemoClass ] {
@@ %s %d-%d
- Constants [0] {
}
- Static properties [0] {
}
- Static methods [0] {
}
- Properties [0] {
}
- Methods [0] {
}
}
array(2) {
[0]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(23) "DelayedTargetValidation"
}
[1]=>
object(ReflectionAttribute)#%d (1) {
["name"]=>
string(22) "AllowDynamicProperties"
}
}
object(AllowDynamicProperties)#%d (0) {
}

View File

@@ -0,0 +1,81 @@
--TEST--
#[\DelayedTargetValidation] with #[\AllowDynamicProperties]: invalid targets don't error
--FILE--
<?php
#[DelayedTargetValidation]
#[AllowDynamicProperties] // Does something here
class DemoClass {
#[DelayedTargetValidation]
#[AllowDynamicProperties] // Does nothing here
public $val;
public string $hooked {
#[DelayedTargetValidation]
#[AllowDynamicProperties] // Does nothing here
get => $this->hooked;
#[DelayedTargetValidation]
#[AllowDynamicProperties] // Does nothing here
set => $value;
}
#[DelayedTargetValidation]
#[AllowDynamicProperties] // Does nothing here
public const CLASS_CONST = 'FOO';
public function __construct(
#[DelayedTargetValidation]
#[AllowDynamicProperties] // Does nothing here
$str
) {
echo "Got: $str\n";
$this->val = $str;
}
#[DelayedTargetValidation]
#[AllowDynamicProperties] // Does nothing here
public function printVal() {
echo 'Value is: ' . $this->val . "\n";
}
}
#[DelayedTargetValidation]
#[AllowDynamicProperties] // Does nothing here
function demoFn() {
echo __FUNCTION__ . "\n";
return 456;
}
#[DelayedTargetValidation]
#[AllowDynamicProperties] // Does nothing here
const GLOBAL_CONST = 'BAR';
$d = new DemoClass('example');
$d->printVal();
var_dump($d->val);
$d->hooked = "foo";
var_dump($d->hooked);
var_dump(DemoClass::CLASS_CONST);
demoFn();
var_dump(GLOBAL_CONST);
$d->missingProp = 'foo';
var_dump($d);
?>
--EXPECTF--
Got: example
Value is: example
string(7) "example"
string(3) "foo"
string(3) "FOO"
demoFn
string(3) "BAR"
object(DemoClass)#%d (3) {
["val"]=>
string(7) "example"
["hooked"]=>
string(3) "foo"
["missingProp"]=>
string(3) "foo"
}

View File

@@ -0,0 +1,95 @@
--TEST--
#[\DelayedTargetValidation] with #[\Attribute]: invalid targets don't error
--FILE--
<?php
class NonAttribute {}
#[DelayedTargetValidation]
#[Attribute] // Does something here
class DemoClass {
#[DelayedTargetValidation]
#[Attribute] // Does nothing here
public $val;
public string $hooked {
#[DelayedTargetValidation]
#[Attribute] // Does nothing here
get => $this->hooked;
#[DelayedTargetValidation]
#[Attribute] // Does nothing here
set => $value;
}
#[DelayedTargetValidation]
#[Attribute] // Does nothing here
public const CLASS_CONST = 'FOO';
public function __construct(
#[DelayedTargetValidation]
#[Attribute] // Does nothing here
$str
) {
echo "Got: $str\n";
$this->val = $str;
}
#[DelayedTargetValidation]
#[Attribute] // Does nothing here
public function printVal() {
echo 'Value is: ' . $this->val . "\n";
}
}
#[DelayedTargetValidation]
#[Attribute] // Does nothing here
function demoFn() {
echo __FUNCTION__ . "\n";
return 456;
}
#[DelayedTargetValidation]
#[Attribute] // Does nothing here
const GLOBAL_CONST = 'BAR';
$d = new DemoClass('example');
$d->printVal();
var_dump($d->val);
$d->hooked = "foo";
var_dump($d->hooked);
var_dump(DemoClass::CLASS_CONST);
demoFn();
var_dump(GLOBAL_CONST);
#[DemoClass('BAZ')]
#[NonAttribute]
class WithDemoAttribs {}
$ref = new ReflectionClass(WithDemoAttribs::class);
$attribs = $ref->getAttributes();
var_dump($attribs[0]->newInstance());
var_dump($attribs[1]->newInstance());
?>
--EXPECTF--
Got: example
Value is: example
string(7) "example"
string(3) "foo"
string(3) "FOO"
demoFn
string(3) "BAR"
Got: BAZ
object(DemoClass)#5 (1) {
["val"]=>
string(3) "BAZ"
["hooked"]=>
uninitialized(string)
}
Fatal error: Uncaught Error: Attempting to use non-attribute class "NonAttribute" as attribute in %s:%d
Stack trace:
#0 %s(%d): ReflectionAttribute->newInstance()
#1 {main}
thrown in %s on line %d

View File

@@ -0,0 +1,82 @@
--TEST--
#[\DelayedTargetValidation] with #[\Deprecated]: valid targets are deprecated
--FILE--
<?php
#[DelayedTargetValidation]
#[Deprecated] // Does nothing here
class DemoClass {
#[DelayedTargetValidation]
#[Deprecated] // Does nothing here
public $val;
public string $hooked {
#[DelayedTargetValidation]
#[Deprecated] // Does something here
get => $this->hooked;
#[DelayedTargetValidation]
#[Deprecated] // Does something here
set => $value;
}
#[DelayedTargetValidation]
#[Deprecated] // Does something here
public const CLASS_CONST = 'FOO';
public function __construct(
#[DelayedTargetValidation]
#[Deprecated] // Does nothing here
$str
) {
echo "Got: $str\n";
$this->val = $str;
}
#[DelayedTargetValidation]
#[Deprecated] // Does something here
public function printVal() {
echo 'Value is: ' . $this->val . "\n";
return 123;
}
}
#[DelayedTargetValidation]
#[Deprecated] // Does something here
function demoFn() {
echo __FUNCTION__ . "\n";
return 456;
}
#[DelayedTargetValidation]
#[Deprecated] // Does something here
const GLOBAL_CONST = 'BAR';
$d = new DemoClass('example');
$d->printVal();
var_dump($d->val);
$d->hooked = "foo";
var_dump($d->hooked);
var_dump(DemoClass::CLASS_CONST);
demoFn();
var_dump(GLOBAL_CONST);
?>
--EXPECTF--
Got: example
Deprecated: Method DemoClass::printVal() is deprecated in %s on line %d
Value is: example
string(7) "example"
Deprecated: Method DemoClass::$hooked::set() is deprecated in %s on line %d
Deprecated: Method DemoClass::$hooked::get() is deprecated in %s on line %d
string(3) "foo"
Deprecated: Constant DemoClass::CLASS_CONST is deprecated in %s on line %d
string(3) "FOO"
Deprecated: Function demoFn() is deprecated in %s on line %d
demoFn
Deprecated: Constant GLOBAL_CONST is deprecated in %s on line %d
string(3) "BAR"

View File

@@ -0,0 +1,80 @@
--TEST--
#[\DelayedTargetValidation] with #[\NoDiscard]: valid targets complain about discarding
--FILE--
<?php
#[DelayedTargetValidation]
#[NoDiscard] // Does nothing here
class DemoClass {
#[DelayedTargetValidation]
#[NoDiscard] // Does nothing here
public $val;
public string $hooked {
#[DelayedTargetValidation]
#[NoDiscard] // Does nothing here
get => $this->hooked;
#[DelayedTargetValidation]
#[NoDiscard] // Does nothing here
set => $value;
}
#[DelayedTargetValidation]
#[NoDiscard] // Does nothing here
public const CLASS_CONST = 'FOO';
public function __construct(
#[DelayedTargetValidation]
#[NoDiscard] // Does nothing here
$str
) {
echo "Got: $str\n";
$this->val = $str;
}
#[DelayedTargetValidation]
#[NoDiscard] // Does something here
public function printVal() {
echo 'Value is: ' . $this->val . "\n";
return 123;
}
}
#[DelayedTargetValidation]
#[NoDiscard] // Does something here
function demoFn() {
echo __FUNCTION__ . "\n";
return 456;
}
#[DelayedTargetValidation]
#[NoDiscard] // Does nothing here
const GLOBAL_CONST = 'BAR';
$d = new DemoClass('example');
$d->printVal();
$v = $d->printVal();
var_dump($d->val);
$d->hooked = "foo";
var_dump($d->hooked);
// NoDiscard does not support property hooks, this should not complain
$d->hooked;
var_dump(DemoClass::CLASS_CONST);
demoFn();
$v = demoFn();
var_dump(GLOBAL_CONST);
?>
--EXPECTF--
Got: example
Warning: The return value of method DemoClass::printVal() should either be used or intentionally ignored by casting it as (void) in %s on line %d
Value is: example
Value is: example
string(7) "example"
string(3) "foo"
string(3) "FOO"
Warning: The return value of function demoFn() should either be used or intentionally ignored by casting it as (void) in %s on line %d
demoFn
demoFn
string(3) "BAR"

View File

@@ -0,0 +1,18 @@
--TEST--
#[\DelayedTargetValidation] with #[\Override]: non-overrides still error (get hook)
--FILE--
<?php
class DemoClass {
public string $hooked {
#[DelayedTargetValidation]
#[Override] // Does something here
get => $this->hooked;
set => $value;
}
}
?>
--EXPECTF--
Fatal error: DemoClass::$hooked::get() has #[\Override] attribute, but no matching parent method exists in %s on line %d

View File

@@ -0,0 +1,18 @@
--TEST--
#[\DelayedTargetValidation] with #[\Override]: non-overrides still error (method)
--FILE--
<?php
class DemoClass {
#[DelayedTargetValidation]
#[Override] // Does something here
public function printVal() {
echo 'Value is: ' . $this->val . "\n";
return 123;
}
}
?>
--EXPECTF--
Fatal error: DemoClass::printVal() has #[\Override] attribute, but no matching parent method exists in %s on line %d

View File

@@ -0,0 +1,15 @@
--TEST--
#[\DelayedTargetValidation] with #[\Override]: non-overrides still error (normal property)
--FILE--
<?php
class DemoClass {
#[DelayedTargetValidation]
#[Override] // Does something here
public string $prop;
}
?>
--EXPECTF--
Fatal error: DemoClass::$prop has #[\Override] attribute, but no matching parent property exists in %s on line %d

View File

@@ -0,0 +1,18 @@
--TEST--
#[\DelayedTargetValidation] with #[\Override]: non-overrides still error (set hook)
--FILE--
<?php
class DemoClass {
public string $hooked {
get => $this->hooked;
#[DelayedTargetValidation]
#[Override] // Does something here
set => $value;
}
}
?>
--EXPECTF--
Fatal error: DemoClass::$hooked::set() has #[\Override] attribute, but no matching parent method exists in %s on line %d

View File

@@ -0,0 +1,84 @@
--TEST--
#[\DelayedTargetValidation] with #[\Override]: invalid targets or actual overrides don't do anything
--FILE--
<?php
class Base {
public $val;
public string $hooked {
get => $this->hooked;
set => $value;
}
public function printVal() {
echo __METHOD__ . "\n";
}
}
#[DelayedTargetValidation]
#[Override] // Does nothing here
class DemoClass extends Base {
#[DelayedTargetValidation]
#[Override] // Does something here
public $val;
public string $hooked {
#[DelayedTargetValidation]
#[Override] // Does something here
get => $this->hooked;
#[DelayedTargetValidation]
#[Override] // Does something here
set => $value;
}
#[DelayedTargetValidation]
#[Override] // Does nothing here
public const CLASS_CONST = 'FOO';
public function __construct(
#[DelayedTargetValidation]
#[Override] // Does nothing here
$str
) {
echo "Got: $str\n";
$this->val = $str;
}
#[DelayedTargetValidation]
#[Override] // Does something here
public function printVal() {
echo 'Value is: ' . $this->val . "\n";
return 123;
}
}
#[DelayedTargetValidation]
#[Override] // Does nothing here
function demoFn() {
echo __FUNCTION__ . "\n";
return 456;
}
#[DelayedTargetValidation]
#[Override] // Does nothing here
const GLOBAL_CONST = 'BAR';
$d = new DemoClass('example');
$d->printVal();
var_dump($d->val);
$d->hooked = "foo";
var_dump($d->hooked);
var_dump(DemoClass::CLASS_CONST);
demoFn();
var_dump(GLOBAL_CONST);
?>
--EXPECT--
Got: example
Value is: example
string(7) "example"
string(3) "foo"
string(3) "FOO"
demoFn
string(3) "BAR"

View File

@@ -0,0 +1,82 @@
--TEST--
#[\DelayedTargetValidation] with #[\ReturnTypeWillChange]: valid targets suppress return type warnings
--FILE--
<?php
class WithoutAttrib implements Countable {
public function count() {
return 5;
}
}
#[DelayedTargetValidation]
#[ReturnTypeWillChange] // Does nothing here
class DemoClass implements Countable {
#[DelayedTargetValidation]
#[ReturnTypeWillChange] // Does nothing here
public $val;
public string $hooked {
#[DelayedTargetValidation]
#[ReturnTypeWillChange] // Does nothing here
get => $this->hooked;
#[DelayedTargetValidation]
#[ReturnTypeWillChange] // Does nothing here
set => $value;
}
#[DelayedTargetValidation]
#[ReturnTypeWillChange] // Does nothing here
public const CLASS_CONST = 'FOO';
public function __construct(
#[DelayedTargetValidation]
#[ReturnTypeWillChange] // Does nothing here
$str
) {
echo "Got: $str\n";
$this->val = $str;
}
#[DelayedTargetValidation]
#[ReturnTypeWillChange] // Does something here
public function printVal() {
echo 'Value is: ' . $this->val . "\n";
}
#[DelayedTargetValidation]
#[ReturnTypeWillChange] // Does something here
public function count() {
return 5;
}
}
#[DelayedTargetValidation]
#[ReturnTypeWillChange] // Does nothing here
function demoFn() {
echo __FUNCTION__ . "\n";
return 456;
}
#[DelayedTargetValidation]
#[ReturnTypeWillChange] // Does nothing here
const GLOBAL_CONST = 'BAR';
$d = new DemoClass('example');
$d->printVal();
var_dump($d->val);
$d->hooked = "foo";
var_dump($d->hooked);
var_dump(DemoClass::CLASS_CONST);
demoFn();
var_dump(GLOBAL_CONST);
?>
--EXPECTF--
Deprecated: Return type of WithoutAttrib::count() should either be compatible with Countable::count(): int, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice in %s on line %d
Got: example
Value is: example
string(7) "example"
string(3) "foo"
string(3) "FOO"
demoFn
string(3) "BAR"

View File

@@ -0,0 +1,82 @@
--TEST--
#[\DelayedTargetValidation] with #[\SensitiveParameter]: parameter still redacted
--FILE--
<?php
#[DelayedTargetValidation]
#[SensitiveParameter] // Does nothing here
class DemoClass {
#[DelayedTargetValidation]
#[SensitiveParameter] // Does nothing here
public $val;
public string $hooked {
#[DelayedTargetValidation]
#[SensitiveParameter] // Does nothing here
get => $this->hooked;
#[DelayedTargetValidation]
#[SensitiveParameter] // Does nothing here
set => $value;
}
#[DelayedTargetValidation]
#[SensitiveParameter] // Does nothing here
public const CLASS_CONST = 'FOO';
public function __construct(
#[DelayedTargetValidation]
#[SensitiveParameter] // Does something here
$str
) {
echo "Got: $str\n";
$this->val = $str;
}
#[DelayedTargetValidation]
#[SensitiveParameter] // Does nothing here
public function printVal(
#[DelayedTargetValidation]
#[SensitiveParameter]
$sensitive // Does something here
) {
throw new Exception('Testing backtrace');
}
}
#[DelayedTargetValidation]
#[SensitiveParameter] // Does nothing here
function demoFn() {
echo __FUNCTION__ . "\n";
return 456;
}
#[DelayedTargetValidation]
#[SensitiveParameter] // Does nothing here
const GLOBAL_CONST = 'BAR';
$d = new DemoClass('example');
var_dump($d->val);
$d->hooked = "foo";
var_dump($d->hooked);
var_dump(DemoClass::CLASS_CONST);
demoFn();
var_dump(GLOBAL_CONST);
$d->printVal('BAZ');
?>
--EXPECTF--
Got: example
string(7) "example"
string(3) "foo"
string(3) "FOO"
demoFn
string(3) "BAR"
Fatal error: Uncaught Exception: Testing backtrace in %s:%d
Stack trace:
#0 %s(%d): DemoClass->printVal(Object(SensitiveParameterValue))
#1 {main}
thrown in %s on line %d

View File

@@ -32,6 +32,7 @@ ZEND_API zend_class_entry *zend_ce_sensitive_parameter_value;
ZEND_API zend_class_entry *zend_ce_override;
ZEND_API zend_class_entry *zend_ce_deprecated;
ZEND_API zend_class_entry *zend_ce_nodiscard;
ZEND_API zend_class_entry *zend_ce_delayed_target_validation;
static zend_object_handlers attributes_object_handlers_sensitive_parameter_value;
@@ -69,33 +70,28 @@ uint32_t zend_attribute_attribute_get_flags(zend_attribute *attr, zend_class_ent
return ZEND_ATTRIBUTE_TARGET_ALL;
}
static void validate_allow_dynamic_properties(
static zend_string *validate_allow_dynamic_properties(
zend_attribute *attr, uint32_t target, zend_class_entry *scope)
{
ZEND_ASSERT(scope != NULL);
const char *msg = NULL;
if (scope->ce_flags & ZEND_ACC_TRAIT) {
zend_error_noreturn(E_ERROR, "Cannot apply #[\\AllowDynamicProperties] to trait %s",
ZSTR_VAL(scope->name)
);
msg = "Cannot apply #[\\AllowDynamicProperties] to trait %s";
} else if (scope->ce_flags & ZEND_ACC_INTERFACE) {
msg = "Cannot apply #[\\AllowDynamicProperties] to interface %s";
} else if (scope->ce_flags & ZEND_ACC_READONLY_CLASS) {
msg = "Cannot apply #[\\AllowDynamicProperties] to readonly class %s";
} else if (scope->ce_flags & ZEND_ACC_ENUM) {
msg = "Cannot apply #[\\AllowDynamicProperties] to enum %s";
}
if (scope->ce_flags & ZEND_ACC_INTERFACE) {
zend_error_noreturn(E_ERROR, "Cannot apply #[\\AllowDynamicProperties] to interface %s",
ZSTR_VAL(scope->name)
);
}
if (scope->ce_flags & ZEND_ACC_READONLY_CLASS) {
zend_error_noreturn(E_ERROR, "Cannot apply #[\\AllowDynamicProperties] to readonly class %s",
ZSTR_VAL(scope->name)
);
}
if (scope->ce_flags & ZEND_ACC_ENUM) {
zend_error_noreturn(E_ERROR, "Cannot apply #[\\AllowDynamicProperties] to enum %s",
ZSTR_VAL(scope->name)
);
if (msg != NULL) {
return zend_strpprintf(0, msg, ZSTR_VAL(scope->name));
}
scope->ce_flags |= ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES;
return NULL;
}
static void validate_attribute(
static zend_string *validate_attribute(
zend_attribute *attr, uint32_t target, zend_class_entry *scope)
{
const char *msg = NULL;
@@ -109,8 +105,9 @@ static void validate_attribute(
msg = "Cannot apply #[\\Attribute] to abstract class %s";
}
if (msg != NULL) {
zend_error_noreturn(E_ERROR, msg, ZSTR_VAL(scope->name));
return zend_strpprintf(0, msg, ZSTR_VAL(scope->name));
}
return NULL;
}
ZEND_METHOD(Attribute, __construct)
@@ -212,6 +209,20 @@ ZEND_METHOD(Deprecated, __construct)
}
}
static zend_string *validate_nodiscard(
zend_attribute *attr, uint32_t target, zend_class_entry *scope)
{
ZEND_ASSERT(CG(in_compilation));
const zend_string *prop_info_name = CG(context).active_property_info_name;
if (prop_info_name != NULL) {
// Applied to a hook
return ZSTR_INIT_LITERAL("#[\\NoDiscard] is not supported for property hooks", 0);
}
zend_op_array *op_array = CG(active_op_array);
op_array->fn_flags |= ZEND_ACC_NODISCARD;
return NULL;
}
ZEND_METHOD(NoDiscard, __construct)
{
zend_string *message = NULL;
@@ -439,6 +450,9 @@ static void attr_free(zval *v)
zend_string_release(attr->name);
zend_string_release(attr->lcname);
if (attr->validation_error != NULL) {
zend_string_release(attr->validation_error);
}
for (uint32_t i = 0; i < attr->argc; i++) {
if (attr->args[i].name) {
@@ -471,6 +485,7 @@ ZEND_API zend_attribute *zend_add_attribute(HashTable **attributes, zend_string
}
attr->lcname = zend_string_tolower_ex(attr->name, persistent);
attr->validation_error = NULL;
attr->flags = flags;
attr->lineno = lineno;
attr->offset = offset;
@@ -567,6 +582,10 @@ void zend_register_attribute_ce(void)
zend_ce_nodiscard = register_class_NoDiscard();
attr = zend_mark_internal_attribute(zend_ce_nodiscard);
attr->validator = validate_nodiscard;
zend_ce_delayed_target_validation = register_class_DelayedTargetValidation();
attr = zend_mark_internal_attribute(zend_ce_delayed_target_validation);
}
void zend_attributes_shutdown(void)

View File

@@ -50,6 +50,7 @@ extern ZEND_API zend_class_entry *zend_ce_sensitive_parameter_value;
extern ZEND_API zend_class_entry *zend_ce_override;
extern ZEND_API zend_class_entry *zend_ce_deprecated;
extern ZEND_API zend_class_entry *zend_ce_nodiscard;
extern ZEND_API zend_class_entry *zend_ce_delayed_target_validation;
typedef struct {
zend_string *name;
@@ -59,6 +60,9 @@ typedef struct {
typedef struct _zend_attribute {
zend_string *name;
zend_string *lcname;
/* Only non-null for internal attributes with validation errors that are
* delayed until runtime via #[\DelayedTargetValidation] */
zend_string *validation_error;
uint32_t flags;
uint32_t lineno;
/* Parameter offsets start at 1, everything else uses 0. */
@@ -70,7 +74,7 @@ typedef struct _zend_attribute {
typedef struct _zend_internal_attribute {
zend_class_entry *ce;
uint32_t flags;
void (*validator)(zend_attribute *attr, uint32_t target, zend_class_entry *scope);
zend_string* (*validator)(zend_attribute *attr, uint32_t target, zend_class_entry *scope);
} zend_internal_attribute;
ZEND_API zend_attribute *zend_get_attribute(HashTable *attributes, zend_string *lcname);

View File

@@ -97,3 +97,9 @@ final class NoDiscard
public function __construct(?string $message = null) {}
}
/**
* @strict-properties
*/
#[Attribute(Attribute::TARGET_ALL)]
final class DelayedTargetValidation {}

View File

@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: 715016d1ff1b0a6abb325a71083eff397a080c44 */
* Stub hash: fa08288df8338c1a16fbf83c179c4084a56007e1 */
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Attribute___construct, 0, 0, 0)
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, flags, IS_LONG, 0, "Attribute::TARGET_ALL")
@@ -276,3 +276,18 @@ static zend_class_entry *register_class_NoDiscard(void)
return class_entry;
}
static zend_class_entry *register_class_DelayedTargetValidation(void)
{
zend_class_entry ce, *class_entry;
INIT_CLASS_ENTRY(ce, "DelayedTargetValidation", NULL);
class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES);
zend_string *attribute_name_Attribute_class_DelayedTargetValidation_0 = zend_string_init_interned("Attribute", sizeof("Attribute") - 1, 1);
zend_attribute *attribute_Attribute_class_DelayedTargetValidation_0 = zend_add_class_attribute(class_entry, attribute_name_Attribute_class_DelayedTargetValidation_0, 1);
zend_string_release(attribute_name_Attribute_class_DelayedTargetValidation_0);
ZVAL_LONG(&attribute_Attribute_class_DelayedTargetValidation_0->args[0].value, ZEND_ATTRIBUTE_TARGET_ALL);
return class_entry;
}

View File

@@ -7557,19 +7557,41 @@ static void zend_compile_attributes(
}
if (*attributes != NULL) {
/* Allow delaying target validation for forward compatibility. */
zend_attribute *delayed_target_validation = NULL;
if (target == ZEND_ATTRIBUTE_TARGET_PARAMETER) {
ZEND_ASSERT(offset >= 1);
/* zend_get_parameter_attribute_str will add 1 too */
delayed_target_validation = zend_get_parameter_attribute_str(
*attributes,
"delayedtargetvalidation",
strlen("delayedtargetvalidation"),
offset - 1
);
} else {
delayed_target_validation = zend_get_attribute_str(
*attributes,
"delayedtargetvalidation",
strlen("delayedtargetvalidation")
);
}
/* Validate attributes in a secondary loop (needed to detect repeated attributes). */
ZEND_HASH_PACKED_FOREACH_PTR(*attributes, attr) {
if (attr->offset != offset || NULL == (config = zend_internal_attribute_get(attr->lcname))) {
continue;
}
bool run_validator = true;
if (!(target & (config->flags & ZEND_ATTRIBUTE_TARGET_ALL))) {
zend_string *location = zend_get_attribute_target_names(target);
zend_string *allowed = zend_get_attribute_target_names(config->flags);
if (delayed_target_validation == NULL) {
zend_string *location = zend_get_attribute_target_names(target);
zend_string *allowed = zend_get_attribute_target_names(config->flags);
zend_error_noreturn(E_ERROR, "Attribute \"%s\" cannot target %s (allowed targets: %s)",
ZSTR_VAL(attr->name), ZSTR_VAL(location), ZSTR_VAL(allowed)
);
zend_error_noreturn(E_ERROR, "Attribute \"%s\" cannot target %s (allowed targets: %s)",
ZSTR_VAL(attr->name), ZSTR_VAL(location), ZSTR_VAL(allowed)
);
}
run_validator = false;
}
if (!(config->flags & ZEND_ATTRIBUTE_IS_REPEATABLE)) {
@@ -7578,8 +7600,17 @@ static void zend_compile_attributes(
}
}
if (config->validator != NULL) {
config->validator(attr, target, CG(active_class_entry));
/* Validators are not run if the target is already invalid */
if (run_validator && config->validator != NULL) {
zend_string *error = config->validator(attr, target, CG(active_class_entry));
if (error != NULL) {
if (delayed_target_validation == NULL) {
zend_error_noreturn(E_COMPILE_ERROR, "%s", ZSTR_VAL(error));
zend_string_efree(error);
} else {
attr->validation_error = error;
}
}
}
} ZEND_HASH_FOREACH_END();
}
@@ -8425,6 +8456,10 @@ static zend_op_array *zend_compile_func_decl_ex(
CG(active_op_array) = op_array;
zend_oparray_context_begin(&orig_oparray_context, op_array);
CG(context).active_property_info_name = property_info_name;
CG(context).active_property_hook_kind = hook_kind;
if (decl->child[4]) {
int target = ZEND_ATTRIBUTE_TARGET_FUNCTION;
@@ -8454,15 +8489,7 @@ static zend_op_array *zend_compile_func_decl_ex(
op_array->fn_flags |= ZEND_ACC_DEPRECATED;
}
zend_attribute *nodiscard_attribute = zend_get_attribute_str(
op_array->attributes,
"nodiscard",
sizeof("nodiscard")-1
);
if (nodiscard_attribute) {
op_array->fn_flags |= ZEND_ACC_NODISCARD;
}
// ZEND_ACC_NODISCARD is added via an attribute validator
}
/* Do not leak the class scope into free standing functions, even if they are dynamically
@@ -8476,10 +8503,6 @@ static zend_op_array *zend_compile_func_decl_ex(
op_array->fn_flags |= ZEND_ACC_TOP_LEVEL;
}
zend_oparray_context_begin(&orig_oparray_context, op_array);
CG(context).active_property_info_name = property_info_name;
CG(context).active_property_hook_kind = hook_kind;
{
/* Push a separator to the loop variable stack */
zend_loop_var dummy_var;
@@ -8514,9 +8537,12 @@ static zend_op_array *zend_compile_func_decl_ex(
}
if (op_array->fn_flags & ZEND_ACC_NODISCARD) {
if (is_hook) {
zend_error_noreturn(E_COMPILE_ERROR, "#[\\NoDiscard] is not supported for property hooks");
}
/* ZEND_ACC_NODISCARD gets added by the attribute validator, but only
* if the method is not a hook; if it is a hook, then the validator
* will have returned an error message, even if the error message was
* delayed with #[\DelayedTargetValidation] that ZEND_ACC_NODISCARD
* flag should not have been added. */
ZEND_ASSERT(!is_hook);
if (op_array->fn_flags & ZEND_ACC_HAS_RETURN_TYPE) {
zend_arg_info *return_info = CG(active_op_array)->arg_info - 1;

View File

@@ -461,6 +461,7 @@ static void zend_file_cache_serialize_attribute(zval *zv,
SERIALIZE_STR(attr->name);
SERIALIZE_STR(attr->lcname);
SERIALIZE_STR(attr->validation_error);
for (i = 0; i < attr->argc; i++) {
SERIALIZE_STR(attr->args[i].name);
@@ -1352,6 +1353,7 @@ static void zend_file_cache_unserialize_attribute(zval *zv, zend_persistent_scri
UNSERIALIZE_STR(attr->name);
UNSERIALIZE_STR(attr->lcname);
UNSERIALIZE_STR(attr->validation_error);
for (i = 0; i < attr->argc; i++) {
UNSERIALIZE_STR(attr->args[i].name);

View File

@@ -311,6 +311,9 @@ static HashTable *zend_persist_attributes(HashTable *attributes)
zend_accel_store_interned_string(copy->name);
zend_accel_store_interned_string(copy->lcname);
if (copy->validation_error) {
zend_accel_store_interned_string(copy->validation_error);
}
for (i = 0; i < copy->argc; i++) {
if (copy->args[i].name) {

View File

@@ -181,6 +181,9 @@ static void zend_persist_attributes_calc(HashTable *attributes)
ADD_SIZE(ZEND_ATTRIBUTE_SIZE(attr->argc));
ADD_INTERNED_STRING(attr->name);
ADD_INTERNED_STRING(attr->lcname);
if (attr->validation_error != NULL) {
ADD_INTERNED_STRING(attr->validation_error);
}
for (i = 0; i < attr->argc; i++) {
if (attr->args[i].name) {

View File

@@ -7321,26 +7321,61 @@ ZEND_METHOD(ReflectionAttribute, newInstance)
RETURN_THROWS();
}
/* This code can be reached under one of three possible conditions:
* - the attribute is an internal attribute, and it had the target and
* and repetition validated already
* - the attribute is an internal attribute and repetition was validated
* already, the internal validator might have been run if the target was
* correct, but any error would have been stored in
* `zend_attribute.validation_error` instead of being thrown due to the
* presence of #[DelayedTargetValidation]
* - the attribute is a user attribute, and neither target nor repetition
* have been validated.
*/
uint32_t flags = zend_attribute_attribute_get_flags(marker, ce);
if (EG(exception)) {
RETURN_THROWS();
}
/* No harm in always running target validation, for internal attributes
* without #[DelayedTargetValidation] it isn't necessary but will always
* succeed. */
if (!(attr->target & flags)) {
zend_string *location = zend_get_attribute_target_names(attr->target);
zend_string *allowed = zend_get_attribute_target_names(flags);
zend_throw_error(NULL, "Attribute \"%s\" cannot target %s (allowed targets: %s)",
ZSTR_VAL(attr->data->name), ZSTR_VAL(location), ZSTR_VAL(allowed)
);
zend_string_release(location);
zend_string_release(allowed);
RETURN_THROWS();
}
if (attr->data->validation_error != NULL) {
/* Delayed validation errors should only be set for internal attributes. */
ZEND_ASSERT(ce->type == ZEND_INTERNAL_CLASS);
/* Delayed validation errors should only be set when
* #[\DelayedTargetValidation] is used. Searching for the attribute is
* more expensive than just an assertion and so we don't worry about it
* for non-debug builds. See discussion on GH-18817. */
#if ZEND_DEBUG
zend_attribute *delayed_target_validation = zend_get_attribute_str(
attr->attributes,
"delayedtargetvalidation",
strlen("delayedtargetvalidation")
);
ZEND_ASSERT(delayed_target_validation != NULL);
#endif
zend_throw_exception(zend_ce_error, ZSTR_VAL(attr->data->validation_error), 0);
RETURN_THROWS();
}
/* Repetition validation is done even if #[DelayedTargetValidation] is used
* and so can be skipped for internal attributes. */
if (ce->type == ZEND_USER_CLASS) {
uint32_t flags = zend_attribute_attribute_get_flags(marker, ce);
if (EG(exception)) {
RETURN_THROWS();
}
if (!(attr->target & flags)) {
zend_string *location = zend_get_attribute_target_names(attr->target);
zend_string *allowed = zend_get_attribute_target_names(flags);
zend_throw_error(NULL, "Attribute \"%s\" cannot target %s (allowed targets: %s)",
ZSTR_VAL(attr->data->name), ZSTR_VAL(location), ZSTR_VAL(allowed)
);
zend_string_release(location);
zend_string_release(allowed);
RETURN_THROWS();
}
if (!(flags & ZEND_ATTRIBUTE_IS_REPEATABLE)) {
if (zend_is_attribute_repeated(attr->attributes, attr->data)) {
zend_throw_error(NULL, "Attribute \"%s\" must not be repeated", ZSTR_VAL(attr->data->name));

View File

@@ -958,11 +958,12 @@ static zend_function *zend_test_class_static_method_get(zend_class_entry *ce, ze
return zend_std_get_static_method(ce, name, NULL);
}
void zend_attribute_validate_zendtestattribute(zend_attribute *attr, uint32_t target, zend_class_entry *scope)
zend_string *zend_attribute_validate_zendtestattribute(zend_attribute *attr, uint32_t target, zend_class_entry *scope)
{
if (target != ZEND_ATTRIBUTE_TARGET_CLASS) {
zend_error(E_COMPILE_ERROR, "Only classes can be marked with #[ZendTestAttribute]");
return ZSTR_INIT_LITERAL("Only classes can be marked with #[ZendTestAttribute]", 0);
}
return NULL;
}
static ZEND_METHOD(_ZendTestClass, __toString)