diff --git a/phpunit/src/EnumMethodDeclarationRulesTest.php b/phpunit/src/EnumMethodDeclarationRulesTest.php index fd4ebb14..0b406c11 100644 --- a/phpunit/src/EnumMethodDeclarationRulesTest.php +++ b/phpunit/src/EnumMethodDeclarationRulesTest.php @@ -143,6 +143,169 @@ PHP; $compiler->composeTraitDeclarations([$file]); } + public function testEnumCannotDeclareAbstractMethod(): void + { + $source = <<<'PHP' +compilerFor($source); + + $this->expectException(TestError::class); + $this->expectExceptionMessage('Enum method Suit::label() must not be abstract'); + $compiler->prepareFile($file); + } + + public function testEnumMustImplementAbstractMethodImportedFromTrait(): void + { + $source = <<<'PHP' +compilerFor($source); + $compiler->prepareFile($file); + + $this->expectException(TestError::class); + $this->expectExceptionMessage('Enum Suit must implement 1 abstract method (Suit::label)'); + $compiler->convertFile($file); + } + + public function testEnumMayImplementAbstractTraitMethod(): void + { + $source = <<<'PHP' +name; + } +} + +function main(): void {} +PHP; + + [$compiler, $file] = $this->compilerFor($source); + $compiler->prepareFile($file); + $compiler->convertFile($file); + + self::assertFileExists($compiler->getCppFile($file)); + } + + public function testEnumMayImplementInterface(): void + { + $source = <<<'PHP' +name; + } +} + +function main(): void {} +PHP; + + [$compiler, $file] = $this->compilerFor($source); + $compiler->prepareFile($file); + $compiler->convertFile($file); + + self::assertFileExists($compiler->getCppFile($file)); + } + + public function testEnumMustImplementInterfaceMethod(): void + { + $source = <<<'PHP' +compilerFor($source); + $compiler->prepareFile($file); + + $this->expectException(TestError::class); + $this->expectExceptionMessage('Enum Suit must implement 1 abstract method (Labeled::label)'); + $compiler->convertFile($file); + } + + public function testEnumReportsTraitAndInterfaceAbstractMethodsTogether(): void + { + $source = <<<'PHP' +compilerFor($source); + $compiler->prepareFile($file); + + $this->expectException(TestError::class); + $this->expectExceptionMessage( + 'Enum Suit must implement 2 abstract methods (Suit::label, SerializableName::serializedName)', + ); + $compiler->convertFile($file); + } + public function testCallCallStaticAndInvokeRemainAllowed(): void { $source = <<<'PHP' diff --git a/src/Preprocessor.php b/src/Preprocessor.php index a04109e5..7ede4171 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -2487,6 +2487,10 @@ class Preprocessor extends CompilerBase $this->assertEnumMayIncludeMethod($v, $name); $flags = $this->parseModifiers($v->flags); $abstract = $flags & Modifiers::ABSTRACT; + if ($class instanceof Node\Stmt\Enum_ && $abstract) { + $enumName = $this->classDef->getNamespacedName(false); + $this->fatalError($v, "Enum method {$enumName}::{$name}() must not be abstract"); + } if ($this->classDef->nativeObject && ($flags & Modifiers::STATIC)) { $this->fatalError($v, 'Native class static methods are not supported'); } diff --git a/src/Translator.php b/src/Translator.php index 396c2c46..fc9a05ea 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -4333,7 +4333,7 @@ CODE; $this->checkInterfaceImplementations($class); $this->checkInheritedConstantContracts($class); $this->checkInterfaceMethodCollisions($class); - $this->checkInheritedAbstractMethodsAreImplemented($class); + $this->checkAbstractMethodsAreImplemented($class); } $code = $this->genNativeMethod($methodCodes); if ($this->classDef->nativeObject) { @@ -5750,6 +5750,12 @@ CODE; if ($classDef->isAbstract()) { continue; } + // Enums cannot be abstract. Defer missing methods so Trait and + // interface requirements can be reported together with Zend's + // "Enum ... must implement N abstract methods" diagnostic. + if ($classDef->enum) { + continue; + } $this->fatalError($node, "Class `{$classDef->getNamespacedName(false)}` must implement method `{$interfaceName}::{$interfaceMethodDef->name}()`"); } $this->validateMethodOverrideSignature( @@ -5892,13 +5898,36 @@ CODE; } } - private function checkInheritedAbstractMethodsAreImplemented(NodeAbstract $node): void + private function checkAbstractMethodsAreImplemented(NodeAbstract $node): void { $classDef = $this->classDef; if ($classDef->isAbstract()) { return; } + if ($classDef->enum) { + $missing = $this->collectMissingEnumAbstractMethods($classDef); + if ($missing !== []) { + $count = count($missing); + $noun = $count === 1 ? 'method' : 'methods'; + $this->fatalError( + $node, + "Enum {$classDef->getNamespacedName(false)} must implement {$count} abstract {$noun} (" . + implode(', ', $missing) . ')', + ); + } + } elseif ($classDef->abstractMethodDefs !== []) { + foreach ($classDef->abstractMethodDefs as $methodDef) { + if ($this->findClassMethodDef($classDef, $methodDef->name, false) === null) { + $this->fatalError( + $node, + "Class `{$classDef->getNamespacedName(false)}` must implement abstract method " . + "`{$classDef->getNamespacedName(false)}::{$methodDef->name}()`", + ); + } + } + } + $current = $classDef; while ($current->extends && $this->hasClass($current->extends)) { $parent = $this->getClass($current->extends); @@ -5914,6 +5943,51 @@ CODE; } } + /** @return list */ + private function collectMissingEnumAbstractMethods(ClassDef $enum): array + { + /** @var array $requirements */ + $requirements = []; + $enumName = $enum->getNamespacedName(false); + foreach ($enum->abstractMethodDefs as $methodDef) { + $name = strtolower($methodDef->name); + if ($this->findClassMethodDef($enum, $methodDef->name, false) === null) { + $requirements[$name] = "{$enumName}::{$methodDef->name}"; + } + } + + foreach ($this->getClassImplementedInterfaces($enum) as $interfaceName) { + if ($this->isInternalInterface($interfaceName)) { + $interface = Reflection::getClass($interfaceName); + if ($interface === null) { + continue; + } + foreach ($interface->getMethods() as $method) { + $name = strtolower($method->getName()); + if (!isset($requirements[$name]) + && $this->findClassMethodDef($enum, $method->getName(), false) === null + ) { + $requirements[$name] = $method->getDeclaringClass()->getName() . '::' . $method->getName(); + } + } + continue; + } + if (!$this->hasInterface($interfaceName)) { + continue; + } + foreach ($this->getInterface($interfaceName)->methods as $methodDef) { + $name = strtolower($methodDef->name); + if (!isset($requirements[$name]) + && $this->findClassMethodDef($enum, $methodDef->name, false) === null + ) { + $requirements[$name] = "{$interfaceName}::{$methodDef->name}"; + } + } + } + + return array_values($requirements); + } + private function getVisibilityRank(int $flags): int { if ($flags & Modifiers::PUBLIC) { diff --git a/tests/compiler/enum/enum-trait-abstract-interface.phpt b/tests/compiler/enum/enum-trait-abstract-interface.phpt new file mode 100644 index 00000000..7c582fd1 --- /dev/null +++ b/tests/compiler/enum/enum-trait-abstract-interface.phpt @@ -0,0 +1,43 @@ +--TEST-- +An enum may satisfy Trait abstract methods and implement interfaces +--FILE-- +label(); + } +} + +enum Suit implements Labeled +{ + use RequiresLabel; + + case Hearts; + + public function label(): string + { + return $this->name; + } +} + +function main(): void +{ + var_dump(Suit::Hearts instanceof Labeled); + var_dump(Suit::Hearts->label()); + var_dump(Suit::Hearts->description()); +} +?> +--EXPECT-- +bool(true) +string(6) "Hearts" +string(11) "case=Hearts"