diff --git a/conf/config.level0.neon b/conf/config.level0.neon index b53cd3127b..37921659b8 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -72,6 +72,7 @@ rules: - PHPStan\Rules\Methods\ExistingClassesInTypehintsRule - PHPStan\Rules\Methods\FinalPrivateMethodRule - PHPStan\Rules\Methods\MethodCallableRule + - PHPStan\Rules\Methods\MissingMagicSerializationMethodsRule - PHPStan\Rules\Methods\MissingMethodImplementationRule - PHPStan\Rules\Methods\MethodAttributesRule - PHPStan\Rules\Methods\StaticMethodCallableRule diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index d9c63aa40d..ed3cf34f61 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -201,4 +201,9 @@ public function strSplitReturnsEmptyArray(): bool return $this->versionId >= 80200; } + public function serializableRequiresMagicMethods(): bool + { + return $this->versionId >= 80100; + } + } diff --git a/src/Rules/Methods/MissingMagicSerializationMethodsRule.php b/src/Rules/Methods/MissingMagicSerializationMethodsRule.php new file mode 100644 index 0000000000..10190123c9 --- /dev/null +++ b/src/Rules/Methods/MissingMagicSerializationMethodsRule.php @@ -0,0 +1,80 @@ + + */ +class MissingMagicSerializationMethodsRule implements Rule +{ + + public function __construct(private PhpVersion $phpversion) + { + } + + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $classReflection = $node->getClassReflection(); + if (!$this->phpversion->serializableRequiresMagicMethods()) { + return []; + } + if (!$classReflection->implementsInterface(Serializable::class)) { + return []; + } + if ($classReflection->isAbstract() || $classReflection->isInterface() || $classReflection->isEnum()) { + return []; + } + + $messages = []; + + try { + $nativeMethods = $classReflection->getNativeReflection()->getMethods(); + } catch (IdentifierNotFound) { + return []; + } + + $missingMagicSerialize = true; + $missingMagicUnserialize = true; + foreach ($nativeMethods as $method) { + if ($method->getName() === '__serialize') { + $missingMagicSerialize = false; + } + if ($method->getName() !== '__unserialize') { + continue; + } + + $missingMagicUnserialize = false; + } + + if ($missingMagicSerialize) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Non-abstract class %s implements the Serializable interface, but does not implement __serialize().', + $classReflection->getDisplayName(), + ))->build(); + } + if ($missingMagicUnserialize) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Non-abstract class %s implements the Serializable interface, but does not implement __unserialize().', + $classReflection->getDisplayName(), + ))->build(); + } + + return $messages; + } + +} diff --git a/tests/PHPStan/Rules/Methods/MissingMagicSerializationMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/MissingMagicSerializationMethodsRuleTest.php new file mode 100644 index 0000000000..0ffc40f24e --- /dev/null +++ b/tests/PHPStan/Rules/Methods/MissingMagicSerializationMethodsRuleTest.php @@ -0,0 +1,39 @@ + + */ +class MissingMagicSerializationMethodsRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new MissingMagicSerializationMethodsRule(new PhpVersion(PHP_VERSION_ID)); + } + + public function testRule(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/missing-serialization.php'], [ + [ + 'Non-abstract class MissingMagicSerializationMethods\myObj implements the Serializable interface, but does not implement __serialize().', + 14, + ], + [ + 'Non-abstract class MissingMagicSerializationMethods\myObj implements the Serializable interface, but does not implement __unserialize().', + 14, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/missing-serialization.php b/tests/PHPStan/Rules/Methods/data/missing-serialization.php new file mode 100644 index 0000000000..a6a367e948 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/missing-serialization.php @@ -0,0 +1,29 @@ += 8.1 + +namespace MissingMagicSerializationMethods; + +use Serializable; + +abstract class abstractObj implements Serializable { + public function serialize() { + } + public function unserialize($data) { + } +} + +class myObj implements Serializable { + public function serialize() { + } + public function unserialize($data) { + } +} + +enum myEnum implements Serializable { + case X; + case Y; + + public function serialize() { + } + public function unserialize($data) { + } +}