From 58c3bb64b6e28f870ffb56c78cb9441c1d6c265d Mon Sep 17 00:00:00 2001 From: Alessio Giacobbe Date: Wed, 2 Sep 2026 03:58:49 +0200 Subject: [PATCH] fix(preprocessor): reject abstract method bodies and abstract private class methods (#62) --skip-tests * fix(preprocessor): reject abstract method bodies and abstract private class methods Two abstract-method rules Zend enforces at compile time were missing: - an abstract method with a body was accepted and the body silently dropped; Zend fatals with "Abstract function A::f() cannot contain body" (applies to classes and traits alike, probed on 8.4.13) - `abstract private function` in a class can never be implemented, since private methods do not participate in overriding; Zend fatals with "Abstract function A::f() cannot be declared private". Traits keep accepting it (allowed since PHP 8.0: the consuming class supplies the private implementation) Zend reports the private-modifier error before the body error when both apply; the checks are ordered to match. * test(preprocessor): cover abstract method declaration rules --- phpunit/code/abstract_rule_body.php | 4 ++++ phpunit/code/abstract_rule_private.php | 4 ++++ .../abstract_rule_private_trait_valid.php | 19 +++++++++++++++ phpunit/src/AbstractMethodDeclarationTest.php | 23 +++++++++++++++++++ src/Preprocessor.php | 11 +++++++++ 5 files changed, 61 insertions(+) create mode 100644 phpunit/code/abstract_rule_body.php create mode 100644 phpunit/code/abstract_rule_private.php create mode 100644 phpunit/code/abstract_rule_private_trait_valid.php create mode 100644 phpunit/src/AbstractMethodDeclarationTest.php diff --git a/phpunit/code/abstract_rule_body.php b/phpunit/code/abstract_rule_body.php new file mode 100644 index 00000000..3a6c558c --- /dev/null +++ b/phpunit/code/abstract_rule_body.php @@ -0,0 +1,4 @@ +run(); + } +} + +class Job +{ + use JobTrait; + + private function run(): void {} +} + +function main() {} diff --git a/phpunit/src/AbstractMethodDeclarationTest.php b/phpunit/src/AbstractMethodDeclarationTest.php new file mode 100644 index 00000000..64c96da7 --- /dev/null +++ b/phpunit/src/AbstractMethodDeclarationTest.php @@ -0,0 +1,23 @@ +exec('Abstract function `Job::run()` cannot contain body', 'abstract_rule_body.php'); + } + + public function testAbstractClassMethodCannotBePrivate(): void + { + $this->exec('Abstract function `Job::run()` cannot be declared private', 'abstract_rule_private.php'); + } + + public function testAbstractPrivateTraitMethodIsAllowed(): void + { + $this->compile('abstract_rule_private_trait_valid.php'); + } +} diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 18b99a6e..dceaadc3 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -2099,6 +2099,17 @@ class Preprocessor extends CompilerBase if ($this->classDef->hasMethod($name) || $this->classDef->hasAbstractMethod($name)) { $this->fatalError($v, "Duplicate method `{$this->method}`"); } + // A private method cannot be overridden, so an abstract private + // method could never be implemented. Traits are exempt since PHP + // 8.0: the consuming class provides the private implementation. + if (!$class instanceof Node\Stmt\Trait_ && ($flags & Modifiers::PRIVATE)) { + $this->fatalError($v, "Abstract function `{$this->class}::{$name}()` cannot be declared private"); + } + // An abstract method declares a signature only; Zend rejects a body + // instead of silently discarding it. + if ($v->stmts !== null) { + $this->fatalError($v, "Abstract function `{$this->class}::{$name}()` cannot contain body"); + } if (!$class instanceof Node\Stmt\Trait_ && isset($class->flags) && !($class->flags & Modifiers::ABSTRACT)) { $this->fatalError($v, "Non-abstract class {$this->class} contains abstract method {$v->name}"); }