fix(preprocessor): enforce property-hook placement rules for class properties (#63) --skip-tests
* fix(preprocessor): enforce property-hook placement rules for class properties
The interface path already validated hook placement; class and trait
properties accepted every combination. parseClassPropertyDef now
mirrors Zend's compile-time rules (probed on 8.4.13, including the
precedence order static -> readonly -> abstract rules):
- hooks on a static property ("Cannot declare hooks for static
property")
- hooks on a readonly property, including properties made readonly by
a `readonly class` ("Hooked properties cannot be readonly")
- `abstract` on a hook-less property ("Only hooked properties may be
declared abstract")
- abstract hooked property with a default value ("Cannot specify
default value for virtual hooked property A::$x")
- abstract hooked property whose hooks all have bodies ("Abstract
property A::$x must specify at least one abstract hook")
- abstract hooked property in a non-abstract class; traits stay exempt
(the consuming class satisfies the hook) and enums are already
rejected by the property ban
- bodiless hook on a non-abstract property, in classes and traits
("Non-abstract property hook must have a body"); previously the
lowering fabricated a concrete backing-store accessor for it
* test(preprocessor): cover property-hook placement rules
* fix(preprocessor): reject abstract-private, abstract-final and final-private hooks
Three hook-level modifier conflicts Zend rejects at compile time were
still accepted by the class/trait property path (all probed on 8.4.13):
- `abstract private int $x { get; }` — an abstract (bodiless) hook must
be implemented by a subclass, which private visibility forbids
("Property hook cannot be both abstract and private"). Unlike abstract
private trait methods, Zend does NOT exempt traits from this rule.
- `abstract public int $x { final get; }` — a bodiless hook must stay
overridable to ever gain a body ("Property hook cannot be both
abstract and final").
- `private int $x { final get => 1; }` — a final hook on a private
property is meaningless because private members cannot be overridden
("Property hook cannot be both final and private").
Diagnostic precedence follows Zend: static, then readonly, then the
per-hook final+private conflict (which wins over both abstract
conflicts: `abstract private int $x { final get; }` reports
final+private), then per bodiless hook abstract+private before
abstract+final, all ahead of the default-value and
at-least-one-abstract-hook rules. Protected abstract hooks remain
legal in classes and traits.
master
parent
ea0ea4414a
commit
c578b1d6b0
15 changed files with 235 additions and 0 deletions
@ -0,0 +1,4 @@ |
||||
<?php |
||||
abstract class Box { abstract public int $x { get => 1; } } |
||||
|
||||
function main() {} |
||||
@ -0,0 +1,4 @@ |
||||
<?php |
||||
abstract class Box { abstract public int $x { final get; } } |
||||
|
||||
function main() {} |
||||
@ -0,0 +1,4 @@ |
||||
<?php |
||||
abstract class Box { abstract public int $x; } |
||||
|
||||
function main() {} |
||||
@ -0,0 +1,4 @@ |
||||
<?php |
||||
class Box { abstract public int $x { get; } } |
||||
|
||||
function main() {} |
||||
@ -0,0 +1,4 @@ |
||||
<?php |
||||
abstract class Box { abstract private int $x { get; } } |
||||
|
||||
function main() {} |
||||
@ -0,0 +1,4 @@ |
||||
<?php |
||||
trait Boxed { abstract private int $x { get; } } |
||||
|
||||
function main() {} |
||||
@ -0,0 +1,6 @@ |
||||
<?php |
||||
abstract class Box { abstract protected int $x { get; } } |
||||
class Crate extends Box { protected int $x { get => 1; } } |
||||
trait Boxed { abstract protected int $y { get; } } |
||||
|
||||
function main() {} |
||||
@ -0,0 +1,4 @@ |
||||
<?php |
||||
class Box { public int $x { get; } } |
||||
|
||||
function main() {} |
||||
@ -0,0 +1,4 @@ |
||||
<?php |
||||
abstract class Box { abstract private int $x { final get; } } |
||||
|
||||
function main() {} |
||||
@ -0,0 +1,4 @@ |
||||
<?php |
||||
class Box { public readonly int $x { get => 1; } } |
||||
|
||||
function main() {} |
||||
@ -0,0 +1,4 @@ |
||||
<?php |
||||
readonly class Box { public int $x { get => 1; } } |
||||
|
||||
function main() {} |
||||
@ -0,0 +1,4 @@ |
||||
<?php |
||||
class Box { public static int $x { get => 1; } } |
||||
|
||||
function main() {} |
||||
@ -0,0 +1,4 @@ |
||||
<?php |
||||
abstract class Box { private int $b = 0; public int $x { get => $this->b; set { $this->b = $value; } } abstract public string $s { get; } } |
||||
|
||||
function main() {} |
||||
@ -0,0 +1,83 @@ |
||||
<?php |
||||
|
||||
/** |
||||
* Zend property-hook placement rules for class/trait properties: no |
||||
* hooks on static or readonly properties, abstract hooked properties |
||||
* only in abstract containers with at least one bodiless hook, and a |
||||
* mandatory body on every non-abstract hook. |
||||
*/ |
||||
class PropertyHookPlacementTest extends BaseTest |
||||
{ |
||||
public function testHooksOnStaticPropertyAreRejected(): void |
||||
{ |
||||
$this->exec('Cannot declare hooks for static property', 'hook_rule_static.php'); |
||||
} |
||||
|
||||
public function testHooksOnReadonlyPropertyAreRejected(): void |
||||
{ |
||||
$this->exec('Hooked properties cannot be readonly', 'hook_rule_readonly.php'); |
||||
} |
||||
|
||||
public function testHooksInReadonlyClassAreRejected(): void |
||||
{ |
||||
$this->exec('Hooked properties cannot be readonly', 'hook_rule_readonly_class.php'); |
||||
} |
||||
|
||||
public function testAbstractHookedPropertyRequiresAbstractClass(): void |
||||
{ |
||||
$this->exec('Non-abstract class `Box` contains abstract hooked property `$x`', 'hook_rule_abstract_nonabstract_class.php'); |
||||
} |
||||
|
||||
public function testAbstractPropertyNeedsAtLeastOneAbstractHook(): void |
||||
{ |
||||
$this->exec('Abstract property `Box::$x` must specify at least one abstract hook', 'hook_rule_abstract_all_bodies.php'); |
||||
} |
||||
|
||||
public function testOnlyHookedPropertiesMayBeAbstract(): void |
||||
{ |
||||
$this->exec('Only hooked properties may be declared abstract', 'hook_rule_abstract_no_hooks.php'); |
||||
} |
||||
|
||||
public function testNonAbstractHookMustHaveBody(): void |
||||
{ |
||||
$this->exec('Non-abstract property hook must have a body', 'hook_rule_bodyless.php'); |
||||
} |
||||
|
||||
public function testWellFormedHooksStillCompile(): void |
||||
{ |
||||
$this->compile('hook_rule_valid.php'); |
||||
} |
||||
|
||||
public function testAbstractPrivateHookIsRejected(): void |
||||
{ |
||||
// An abstract (bodiless) hook must be implementable by a subclass, |
||||
// which a private property forbids. |
||||
$this->exec('Property hook cannot be both abstract and private', 'hook_rule_abstract_private.php'); |
||||
} |
||||
|
||||
public function testAbstractPrivateHookIsRejectedInTrait(): void |
||||
{ |
||||
// Unlike abstract private trait methods, Zend does not exempt traits |
||||
// from the abstract-private hook conflict. |
||||
$this->exec('Property hook cannot be both abstract and private', 'hook_rule_abstract_private_trait.php'); |
||||
} |
||||
|
||||
public function testAbstractFinalHookIsRejected(): void |
||||
{ |
||||
// A bodiless hook must be overridable to ever gain a body; it cannot |
||||
// carry final. |
||||
$this->exec('Property hook cannot be both abstract and final', 'hook_rule_abstract_final.php'); |
||||
} |
||||
|
||||
public function testFinalPrivateHookWinsDiagnosticPrecedence(): void |
||||
{ |
||||
// `abstract private int $x { final get; }` violates all three rules; |
||||
// Zend reports the final+private conflict first (probed on 8.4.13). |
||||
$this->exec('Property hook cannot be both final and private', 'hook_rule_final_private.php'); |
||||
} |
||||
|
||||
public function testProtectedAbstractHookStaysLegal(): void |
||||
{ |
||||
$this->compile('hook_rule_abstract_protected_valid.php'); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue