test(enum): add comprehensive enum method declaration rules tests

- Add test cases for forbidden magic methods in enums (__construct, __destruct, __clone, etc.)
- Create tests for trait-injected forbidden methods failing during composition
- Add tests for trait aliases to forbidden methods detection
- Verify __call, __callStatic and __invoke remain allowed in enums
- Include proper setup and teardown for temporary test directories
- Provide data provider for forbidden magic method testing scenarios

feat(enum): enforce forbidden magic method restrictions on enums

- Add ENUM_FORBIDDEN_MAGIC_METHODS constant with restricted methods list
- Implement assertEnumMayIncludeMethod to validate enum method declarations
- Prevent forbidden magic methods from being added to enums during preprocessing
- Share validation logic between direct method declarations and trait composition
- Add
master
韩天峰 2 days ago
parent adc3162a22
commit 5f75c5b48c
  1. 193
      phpunit/src/EnumMethodDeclarationRulesTest.php
  2. 41
      src/Preprocessor.php
  3. 1
      src/Translator.php

@ -0,0 +1,193 @@
<?php
/**
* This file is part of TypePHP(AOT).
*
* @link https://www.swoole.com/aot/
* @contact service@swoole.com
*/
use TypePhp\CompilerTest;
use TypePhp\Exception\TestError;
/**
* Enum method restrictions are declaration rules. They must fail in the
* TypePHP front end, including methods injected by Trait composition, rather
* than reaching Zend class registration during module startup.
* @internal
* @coversNothing
*/
final class EnumMethodDeclarationRulesTest extends PHPUnit\Framework\TestCase
{
private string $testRoot;
protected function setUp(): void
{
$this->testRoot = sys_get_temp_dir() . '/typephp-enum-method-' . bin2hex(random_bytes(8));
mkdir($this->testRoot, 0777, true);
}
protected function tearDown(): void
{
if (!is_dir($this->testRoot)) {
return;
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($this->testRoot, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST,
);
foreach ($iterator as $entry) {
if ($entry->isDir()) {
rmdir($entry->getPathname());
} else {
unlink($entry->getPathname());
}
}
rmdir($this->testRoot);
}
/**
* @dataProvider forbiddenMagicMethodProvider
*/
public function testForbiddenMagicMethodFailsDuringPrepare(string $method): void
{
$source = <<<PHP
<?php
enum Suit
{
case Hearts;
public function {$method}() {}
}
function main(): void {}
PHP;
[$compiler, $file] = $this->compilerFor($source);
$this->expectException(TestError::class);
$this->expectExceptionMessage("Enum `Suit` cannot include magic method `{$method}`");
$compiler->prepareFile($file);
}
public static function forbiddenMagicMethodProvider(): iterable
{
foreach ([
'__construct',
'__destruct',
'__clone',
'__get',
'__set',
'__unset',
'__isset',
'__sleep',
'__wakeup',
'__set_state',
'__serialize',
'__unserialize',
'__ToString',
'__debugInfo',
] as $method) {
yield $method => [$method];
}
}
public function testTraitInjectedForbiddenMethodFailsDuringComposition(): void
{
$source = <<<'PHP'
<?php
trait Builder
{
public function __construct() {}
}
enum Suit
{
use Builder;
case Hearts;
}
function main(): void {}
PHP;
[$compiler, $file] = $this->compilerFor($source);
$compiler->prepareFile($file);
$this->expectException(TestError::class);
$this->expectExceptionMessage('Enum `Suit` cannot include magic method `__construct`');
$compiler->composeTraitDeclarations([$file]);
}
public function testTraitAliasToForbiddenMethodFailsDuringComposition(): void
{
$source = <<<'PHP'
<?php
trait Builder
{
public function cleanup(): void {}
}
enum Suit
{
use Builder { cleanup as __destruct; }
case Hearts;
}
function main(): void {}
PHP;
[$compiler, $file] = $this->compilerFor($source);
$compiler->prepareFile($file);
$this->expectException(TestError::class);
$this->expectExceptionMessage('Enum `Suit` cannot include magic method `__destruct`');
$compiler->composeTraitDeclarations([$file]);
}
public function testCallCallStaticAndInvokeRemainAllowed(): void
{
$source = <<<'PHP'
<?php
enum Suit
{
case Hearts;
public function __call(string $name, array $arguments): mixed
{
return null;
}
public static function __callStatic(string $name, array $arguments): mixed
{
return null;
}
public function __invoke(): string
{
return 'Hearts';
}
}
function main(): void {}
PHP;
[$compiler, $file] = $this->compilerFor($source);
$compiler->prepareFile($file);
$compiler->convertFile($file);
self::assertFileExists($compiler->getCppFile($file));
}
/** @return array{CompilerTest, string} */
private function compilerFor(string $source): array
{
$file = $this->testRoot . '/program.php';
file_put_contents($file, $source);
global $translator;
$compiler = CompilerTest::create($this->testRoot);
$translator = $compiler;
$compiler->addFiles([$file]);
return [$compiler, $file];
}
}

@ -46,8 +46,48 @@ use PhpParser\NodeVisitor\NameResolver;
class Preprocessor extends CompilerBase
{
/**
* Magic methods forbidden on PHP enums. Enum cases are runtime-managed
* singletons, so construction, cloning, destruction, serialization and
* magic property/string/debug handlers cannot be supplied by user code.
* __call, __callStatic and __invoke remain valid enum methods.
*/
private const array ENUM_FORBIDDEN_MAGIC_METHODS = [
'__construct' => true,
'__destruct' => true,
'__clone' => true,
'__get' => true,
'__set' => true,
'__unset' => true,
'__isset' => true,
'__sleep' => true,
'__wakeup' => true,
'__set_state' => true,
'__serialize' => true,
'__unserialize' => true,
'__tostring' => true,
'__debuginfo' => true,
];
protected string $targetName = 'app';
/**
* Validate every method that will become part of an enum. This is shared
* with the Trait-composition phase so a Trait method or alias cannot defer
* the error to Zend class registration at runtime.
*/
protected function assertEnumMayIncludeMethod(Node $node, string $name): void
{
if (!$this->classDef->enum || !isset(self::ENUM_FORBIDDEN_MAGIC_METHODS[strtolower($name)])) {
return;
}
$this->fatalError(
$node,
"Enum `{$this->classDef->getNamespacedName(false)}` cannot include magic method `{$name}`",
);
}
/**
* Discover Native class names before parsing any signatures or fields.
*
@ -2328,6 +2368,7 @@ class Preprocessor extends CompilerBase
$this->method = $name;
$this->assertKeywordMethodMayBeDeclared($v, $name, $this->classDef->nativeObject);
$this->assertNativeMagicMethodSupported($v, $name);
$this->assertEnumMayIncludeMethod($v, $name);
$flags = $this->parseModifiers($v->flags);
$abstract = $flags & Modifiers::ABSTRACT;
if ($this->classDef->nativeObject && ($flags & Modifiers::STATIC)) {

@ -6652,6 +6652,7 @@ CODE;
{
$name = $methodStmt->name->toString();
$this->assertNativeMagicMethodSupported($methodStmt, $name);
$this->assertEnumMayIncludeMethod($methodStmt, $name);
if ($this->classDef->hasMethod($name) || $this->classDef->hasAbstractMethod($name)) {
return;
}

Loading…
Cancel
Save