diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index bbe55e02bbe79..d10d320f4ef62 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -475,7 +475,7 @@ jobs: jitType: tracing runTestsParameters: >- -d opcache.enable_cli=1 - - uses: codecov/codecov-action@v4 + - uses: codecov/codecov-action@v5 if: ${{ !cancelled() }} with: fail_ci_if_error: true diff --git a/UPGRADING b/UPGRADING index cf146f6fa16d6..6d59134c237cf 100644 --- a/UPGRADING +++ b/UPGRADING @@ -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 diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS index c8f4584e11058..acfe30e853559 100644 --- a/UPGRADING.INTERNALS +++ b/UPGRADING.INTERNALS @@ -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 diff --git a/Zend/tests/attributes/delayed_target_validation/has_runtime_errors.phpt b/Zend/tests/attributes/delayed_target_validation/has_runtime_errors.phpt new file mode 100644 index 0000000000000..b1efb538b328b --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/has_runtime_errors.phpt @@ -0,0 +1,282 @@ +--TEST-- +#[\DelayedTargetValidation] has errors at runtime +--FILE-- + $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 [ 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 [ public method __construct ] { + @@ %s %d - %d + + - Parameters [1] { + Parameter #0 [ 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 [ 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 [ public method $v2::set ] { + @@ %s %d - %d + + - Parameters [1] { + Parameter #0 [ 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 [ public method __construct ] { + @@ %s %d - %d + + - Parameters [1] { + Parameter #0 [ 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 [ 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 [ 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) diff --git a/Zend/tests/attributes/delayed_target_validation/no_compile_errors.phpt b/Zend/tests/attributes/delayed_target_validation/no_compile_errors.phpt new file mode 100644 index 0000000000000..b2e14a235f8a5 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/no_compile_errors.phpt @@ -0,0 +1,55 @@ +--TEST-- +#[\DelayedTargetValidation] prevents target errors at compile time +--FILE-- + $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 diff --git a/Zend/tests/attributes/delayed_target_validation/opcache_validator_errors.inc b/Zend/tests/attributes/delayed_target_validation/opcache_validator_errors.inc new file mode 100644 index 0000000000000..275df500c7e93 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/opcache_validator_errors.inc @@ -0,0 +1,5 @@ + +--FILE-- +getAttributes(); +var_dump($attributes); +try { + $attributes[1]->newInstance(); +} catch (Error $e) { + echo get_class($e) . ": " . $e->getMessage() . "\n"; +} + +?> +--EXPECTF-- +Trait [ 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 diff --git a/Zend/tests/attributes/delayed_target_validation/repetition_errors.phpt b/Zend/tests/attributes/delayed_target_validation/repetition_errors.phpt new file mode 100644 index 0000000000000..5c8f9bfc9dde2 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/repetition_errors.phpt @@ -0,0 +1,13 @@ +--TEST-- +#[\DelayedTargetValidation] does not prevent repetition errors +--FILE-- + +--EXPECTF-- +Fatal error: Attribute "NoDiscard" must not be repeated in %s on line %d diff --git a/Zend/tests/attributes/delayed_target_validation/validator_AllowDynamicProperties.phpt b/Zend/tests/attributes/delayed_target_validation/validator_AllowDynamicProperties.phpt new file mode 100644 index 0000000000000..63add9e445a91 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/validator_AllowDynamicProperties.phpt @@ -0,0 +1,180 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\AllowDynamicProperties]: validator errors delayed +--FILE-- +getAttributes(); + var_dump($attributes); + try { + $attributes[1]->newInstance(); + } catch (Error $e) { + echo get_class($e) . ": " . $e->getMessage() . "\n"; + } +} + +?> +--EXPECTF-- +******************** +Trait [ 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 [ 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 [ 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 [ enum DemoEnum implements UnitEnum ] { + @@ %s %d-%d + + - Constants [0] { + } + + - Static properties [0] { + } + + - Static methods [1] { + Method [ 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 diff --git a/Zend/tests/attributes/delayed_target_validation/validator_Attribute.phpt b/Zend/tests/attributes/delayed_target_validation/validator_Attribute.phpt new file mode 100644 index 0000000000000..0571024f19ced --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/validator_Attribute.phpt @@ -0,0 +1,180 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\Attribute]: validator errors delayed +--FILE-- +getAttributes(); + var_dump($attributes); + try { + $attributes[1]->newInstance(); + } catch (Error $e) { + echo get_class($e) . ": " . $e->getMessage() . "\n"; + } +} + +?> +--EXPECTF-- +******************** +Trait [ 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 [ 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 [ 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 [ enum DemoEnum implements UnitEnum ] { + @@ %s %d-%d + + - Constants [0] { + } + + - Static properties [0] { + } + + - Static methods [1] { + Method [ 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 diff --git a/Zend/tests/attributes/delayed_target_validation/validator_NoDiscard.phpt b/Zend/tests/attributes/delayed_target_validation/validator_NoDiscard.phpt new file mode 100644 index 0000000000000..e3cfe9d1c1095 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/validator_NoDiscard.phpt @@ -0,0 +1,79 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\NoDiscard]: validator errors delayed +--FILE-- + $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 [ 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 [ public method $hooked::set ] { + @@ %s %d - %d + + - Parameters [1] { + Parameter #0 [ 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 diff --git a/Zend/tests/attributes/delayed_target_validation/validator_success.phpt b/Zend/tests/attributes/delayed_target_validation/validator_success.phpt new file mode 100644 index 0000000000000..5e2a128dd431a --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/validator_success.phpt @@ -0,0 +1,62 @@ +--TEST-- +#[\DelayedTargetValidation] with a successful validator +--FILE-- +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 [ 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) { +} diff --git a/Zend/tests/attributes/delayed_target_validation/with_AllowDynamicProperties.phpt b/Zend/tests/attributes/delayed_target_validation/with_AllowDynamicProperties.phpt new file mode 100644 index 0000000000000..83f738491c57e --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/with_AllowDynamicProperties.phpt @@ -0,0 +1,81 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\AllowDynamicProperties]: invalid targets don't error +--FILE-- + $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" +} diff --git a/Zend/tests/attributes/delayed_target_validation/with_Attribute.phpt b/Zend/tests/attributes/delayed_target_validation/with_Attribute.phpt new file mode 100644 index 0000000000000..edc7d2a7905fb --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/with_Attribute.phpt @@ -0,0 +1,95 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\Attribute]: invalid targets don't error +--FILE-- + $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 diff --git a/Zend/tests/attributes/delayed_target_validation/with_Deprecated.phpt b/Zend/tests/attributes/delayed_target_validation/with_Deprecated.phpt new file mode 100644 index 0000000000000..093b0abb08e0c --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/with_Deprecated.phpt @@ -0,0 +1,82 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\Deprecated]: valid targets are deprecated +--FILE-- + $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" diff --git a/Zend/tests/attributes/delayed_target_validation/with_NoDiscard.phpt b/Zend/tests/attributes/delayed_target_validation/with_NoDiscard.phpt new file mode 100644 index 0000000000000..f2571ec07c2f1 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/with_NoDiscard.phpt @@ -0,0 +1,80 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\NoDiscard]: valid targets complain about discarding +--FILE-- + $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" diff --git a/Zend/tests/attributes/delayed_target_validation/with_Override_error_get.phpt b/Zend/tests/attributes/delayed_target_validation/with_Override_error_get.phpt new file mode 100644 index 0000000000000..a33e83d517a30 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/with_Override_error_get.phpt @@ -0,0 +1,18 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\Override]: non-overrides still error (get hook) +--FILE-- + $this->hooked; + set => $value; + } +} + +?> +--EXPECTF-- +Fatal error: DemoClass::$hooked::get() has #[\Override] attribute, but no matching parent method exists in %s on line %d diff --git a/Zend/tests/attributes/delayed_target_validation/with_Override_error_method.phpt b/Zend/tests/attributes/delayed_target_validation/with_Override_error_method.phpt new file mode 100644 index 0000000000000..ecca1daff0fd3 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/with_Override_error_method.phpt @@ -0,0 +1,18 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\Override]: non-overrides still error (method) +--FILE-- +val . "\n"; + return 123; + } +} + +?> +--EXPECTF-- +Fatal error: DemoClass::printVal() has #[\Override] attribute, but no matching parent method exists in %s on line %d diff --git a/Zend/tests/attributes/delayed_target_validation/with_Override_error_prop.phpt b/Zend/tests/attributes/delayed_target_validation/with_Override_error_prop.phpt new file mode 100644 index 0000000000000..3a44fa9b22cda --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/with_Override_error_prop.phpt @@ -0,0 +1,15 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\Override]: non-overrides still error (normal property) +--FILE-- + +--EXPECTF-- +Fatal error: DemoClass::$prop has #[\Override] attribute, but no matching parent property exists in %s on line %d diff --git a/Zend/tests/attributes/delayed_target_validation/with_Override_error_set.phpt b/Zend/tests/attributes/delayed_target_validation/with_Override_error_set.phpt new file mode 100644 index 0000000000000..d99580646c548 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/with_Override_error_set.phpt @@ -0,0 +1,18 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\Override]: non-overrides still error (set hook) +--FILE-- + $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 diff --git a/Zend/tests/attributes/delayed_target_validation/with_Override_okay.phpt b/Zend/tests/attributes/delayed_target_validation/with_Override_okay.phpt new file mode 100644 index 0000000000000..dd077f4b9cbd3 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/with_Override_okay.phpt @@ -0,0 +1,84 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\Override]: invalid targets or actual overrides don't do anything +--FILE-- + $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" diff --git a/Zend/tests/attributes/delayed_target_validation/with_ReturnTypeWillChange.phpt b/Zend/tests/attributes/delayed_target_validation/with_ReturnTypeWillChange.phpt new file mode 100644 index 0000000000000..c562075cd6c05 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/with_ReturnTypeWillChange.phpt @@ -0,0 +1,82 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\ReturnTypeWillChange]: valid targets suppress return type warnings +--FILE-- + $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" diff --git a/Zend/tests/attributes/delayed_target_validation/with_SensitiveParameter.phpt b/Zend/tests/attributes/delayed_target_validation/with_SensitiveParameter.phpt new file mode 100644 index 0000000000000..270c031f6bbf0 --- /dev/null +++ b/Zend/tests/attributes/delayed_target_validation/with_SensitiveParameter.phpt @@ -0,0 +1,82 @@ +--TEST-- +#[\DelayedTargetValidation] with #[\SensitiveParameter]: parameter still redacted +--FILE-- + $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 diff --git a/Zend/zend_attributes.c b/Zend/zend_attributes.c index 3256e220d8f3a..4777e5ca08ad1 100644 --- a/Zend/zend_attributes.c +++ b/Zend/zend_attributes.c @@ -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) - ); - } - 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) - ); + 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_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) diff --git a/Zend/zend_attributes.h b/Zend/zend_attributes.h index a4d6b28c0094a..10227c2d1e8ef 100644 --- a/Zend/zend_attributes.h +++ b/Zend/zend_attributes.h @@ -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); diff --git a/Zend/zend_attributes.stub.php b/Zend/zend_attributes.stub.php index 242b751160864..6db68d4d418e2 100644 --- a/Zend/zend_attributes.stub.php +++ b/Zend/zend_attributes.stub.php @@ -97,3 +97,9 @@ final class NoDiscard public function __construct(?string $message = null) {} } + +/** + * @strict-properties + */ +#[Attribute(Attribute::TARGET_ALL)] +final class DelayedTargetValidation {} diff --git a/Zend/zend_attributes_arginfo.h b/Zend/zend_attributes_arginfo.h index 5e7f581dd2226..a271df8e91de9 100644 --- a/Zend/zend_attributes_arginfo.h +++ b/Zend/zend_attributes_arginfo.h @@ -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; +} diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 020b63e362be8..205023fa69b64 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -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; diff --git a/ext/opcache/zend_file_cache.c b/ext/opcache/zend_file_cache.c index 4b47bb54e7acd..4ec285be84348 100644 --- a/ext/opcache/zend_file_cache.c +++ b/ext/opcache/zend_file_cache.c @@ -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); diff --git a/ext/opcache/zend_persist.c b/ext/opcache/zend_persist.c index eb339ee5a4f4b..38e58d5a16632 100644 --- a/ext/opcache/zend_persist.c +++ b/ext/opcache/zend_persist.c @@ -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) { diff --git a/ext/opcache/zend_persist_calc.c b/ext/opcache/zend_persist_calc.c index 639d7d5446705..106a69f5dd383 100644 --- a/ext/opcache/zend_persist_calc.c +++ b/ext/opcache/zend_persist_calc.c @@ -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) { diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index b5ca21d500a82..ab89c47bbb21a 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -7321,26 +7321,61 @@ ZEND_METHOD(ReflectionAttribute, newInstance) RETURN_THROWS(); } - if (ce->type == ZEND_USER_CLASS) { - uint32_t flags = zend_attribute_attribute_get_flags(marker, ce); - if (EG(exception)) { - 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(); + } - if (!(attr->target & flags)) { - zend_string *location = zend_get_attribute_target_names(attr->target); - zend_string *allowed = zend_get_attribute_target_names(flags); + /* 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_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); + zend_string_release(location); + zend_string_release(allowed); - RETURN_THROWS(); - } + 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) { 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)); diff --git a/ext/zend_test/test.c b/ext/zend_test/test.c index c2e5ae6130e7d..af60dffb4bf14 100644 --- a/ext/zend_test/test.c +++ b/ext/zend_test/test.c @@ -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)