fix(enum): enforce abstract method contracts

master
韩天峰 1 day ago
parent 939d04d9df
commit 1c0c02e57e
  1. 163
      phpunit/src/EnumMethodDeclarationRulesTest.php
  2. 4
      src/Preprocessor.php
  3. 78
      src/Translator.php
  4. 43
      tests/compiler/enum/enum-trait-abstract-interface.phpt

@ -143,6 +143,169 @@ PHP;
$compiler->composeTraitDeclarations([$file]); $compiler->composeTraitDeclarations([$file]);
} }
public function testEnumCannotDeclareAbstractMethod(): void
{
$source = <<<'PHP'
<?php
enum Suit
{
case Hearts;
abstract public function label(): string;
}
function main(): void {}
PHP;
[$compiler, $file] = $this->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'
<?php
trait Labeled
{
abstract public function label(): string;
}
enum Suit
{
use Labeled;
case Hearts;
}
function main(): void {}
PHP;
[$compiler, $file] = $this->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'
<?php
trait Labeled
{
abstract public function label(): string;
}
enum Suit
{
use Labeled;
case Hearts;
public function label(): string
{
return $this->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'
<?php
interface Labeled
{
public function label(): string;
}
enum Suit implements Labeled
{
case Hearts;
public function label(): string
{
return $this->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'
<?php
interface Labeled
{
public function label(): string;
}
enum Suit implements Labeled
{
case Hearts;
}
function main(): void {}
PHP;
[$compiler, $file] = $this->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'
<?php
trait Labeled
{
abstract public function label(): string;
}
interface SerializableName
{
public function serializedName(): string;
}
enum Suit implements SerializableName
{
use Labeled;
case Hearts;
}
function main(): void {}
PHP;
[$compiler, $file] = $this->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 public function testCallCallStaticAndInvokeRemainAllowed(): void
{ {
$source = <<<'PHP' $source = <<<'PHP'

@ -2487,6 +2487,10 @@ class Preprocessor extends CompilerBase
$this->assertEnumMayIncludeMethod($v, $name); $this->assertEnumMayIncludeMethod($v, $name);
$flags = $this->parseModifiers($v->flags); $flags = $this->parseModifiers($v->flags);
$abstract = $flags & Modifiers::ABSTRACT; $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)) { if ($this->classDef->nativeObject && ($flags & Modifiers::STATIC)) {
$this->fatalError($v, 'Native class static methods are not supported'); $this->fatalError($v, 'Native class static methods are not supported');
} }

@ -4333,7 +4333,7 @@ CODE;
$this->checkInterfaceImplementations($class); $this->checkInterfaceImplementations($class);
$this->checkInheritedConstantContracts($class); $this->checkInheritedConstantContracts($class);
$this->checkInterfaceMethodCollisions($class); $this->checkInterfaceMethodCollisions($class);
$this->checkInheritedAbstractMethodsAreImplemented($class); $this->checkAbstractMethodsAreImplemented($class);
} }
$code = $this->genNativeMethod($methodCodes); $code = $this->genNativeMethod($methodCodes);
if ($this->classDef->nativeObject) { if ($this->classDef->nativeObject) {
@ -5750,6 +5750,12 @@ CODE;
if ($classDef->isAbstract()) { if ($classDef->isAbstract()) {
continue; 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->fatalError($node, "Class `{$classDef->getNamespacedName(false)}` must implement method `{$interfaceName}::{$interfaceMethodDef->name}()`");
} }
$this->validateMethodOverrideSignature( $this->validateMethodOverrideSignature(
@ -5892,13 +5898,36 @@ CODE;
} }
} }
private function checkInheritedAbstractMethodsAreImplemented(NodeAbstract $node): void private function checkAbstractMethodsAreImplemented(NodeAbstract $node): void
{ {
$classDef = $this->classDef; $classDef = $this->classDef;
if ($classDef->isAbstract()) { if ($classDef->isAbstract()) {
return; 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; $current = $classDef;
while ($current->extends && $this->hasClass($current->extends)) { while ($current->extends && $this->hasClass($current->extends)) {
$parent = $this->getClass($current->extends); $parent = $this->getClass($current->extends);
@ -5914,6 +5943,51 @@ CODE;
} }
} }
/** @return list<string> */
private function collectMissingEnumAbstractMethods(ClassDef $enum): array
{
/** @var array<string, string> $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 private function getVisibilityRank(int $flags): int
{ {
if ($flags & Modifiers::PUBLIC) { if ($flags & Modifiers::PUBLIC) {

@ -0,0 +1,43 @@
--TEST--
An enum may satisfy Trait abstract methods and implement interfaces
--FILE--
<?php
interface Labeled
{
public function label(): string;
}
trait RequiresLabel
{
abstract public function label(): string;
public function description(): string
{
return 'case=' . $this->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"
Loading…
Cancel
Save