diff --git a/config.xsd b/config.xsd index 00caa5508..5b4744f49 100644 --- a/config.xsd +++ b/config.xsd @@ -264,6 +264,7 @@ + diff --git a/docs/running_psalm/error_levels.md b/docs/running_psalm/error_levels.md index 9eeb12e7f..a0799b892 100644 --- a/docs/running_psalm/error_levels.md +++ b/docs/running_psalm/error_levels.md @@ -45,6 +45,7 @@ Level 5 and above allows a more non-verifiable code, and higher levels are even - [InaccessibleProperty](issues/InaccessibleProperty.md) - [InterfaceInstantiation](issues/InterfaceInstantiation.md) - [InvalidAttribute](issues/InvalidAttribute.md) + - [InvalidEnumMethod](issues/InvalidEnumMethod.md) - [InvalidExtendClass](issues/InvalidExtendClass.md) - [InvalidGlobal](issues/InvalidGlobal.md) - [InvalidParamDefault](issues/InvalidParamDefault.md) diff --git a/docs/running_psalm/issues.md b/docs/running_psalm/issues.md index 6729a85fe..d84f2bf3c 100644 --- a/docs/running_psalm/issues.md +++ b/docs/running_psalm/issues.md @@ -69,6 +69,7 @@ - [InvalidDocblockParamName](issues/InvalidDocblockParamName.md) - [InvalidEnumBackingType](issues/InvalidEnumBackingType.md) - [InvalidEnumCaseValue](issues/InvalidEnumCaseValue.md) + - [InvalidEnumMethod](issues/InvalidEnumMethod.md) - [InvalidExtendClass](issues/InvalidExtendClass.md) - [InvalidFalsableReturnType](issues/InvalidFalsableReturnType.md) - [InvalidFunctionCall](issues/InvalidFunctionCall.md) diff --git a/docs/running_psalm/issues/InvalidEnumMethod.md b/docs/running_psalm/issues/InvalidEnumMethod.md new file mode 100644 index 000000000..575cd6b77 --- /dev/null +++ b/docs/running_psalm/issues/InvalidEnumMethod.md @@ -0,0 +1,15 @@ +# InvalidEnumMethod + +Enums may not define most of the magic methods like `__get`, `__toString`, etc. + +```php +is_enum) { + MethodAnalyzer::checkForbiddenEnumMethod($storage); + } + if (!$context->calling_method_id || !$context->collect_initializations) { $context->calling_method_id = strtolower((string)$method_id); } diff --git a/src/Psalm/Internal/Analyzer/MethodAnalyzer.php b/src/Psalm/Internal/Analyzer/MethodAnalyzer.php index 9ade34375..e11abef51 100644 --- a/src/Psalm/Internal/Analyzer/MethodAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/MethodAnalyzer.php @@ -9,6 +9,7 @@ use Psalm\Codebase; use Psalm\Context; use Psalm\Internal\Codebase\InternalCallMapHandler; use Psalm\Internal\MethodIdentifier; +use Psalm\Issue\InvalidEnumMethod; use Psalm\Issue\InvalidStaticInvocation; use Psalm\Issue\MethodSignatureMustOmitReturnType; use Psalm\Issue\NonStaticSelfCall; @@ -27,6 +28,24 @@ use function strtolower; */ class MethodAnalyzer extends FunctionLikeAnalyzer { + // https://github.com/php/php-src/blob/a83923044c48982c80804ae1b45e761c271966d3/Zend/zend_enum.c#L77-L95 + private const FORBIDDEN_ENUM_METHODS = [ + '__construct', + '__destruct', + '__clone', + '__get', + '__set', + '__unset', + '__isset', + '__tostring', + '__debuginfo', + '__serialize', + '__unserialize', + '__sleep', + '__wakeup', + '__set_state', + ]; + /** @psalm-external-mutation-free */ public function __construct( PhpParser\Node\Stmt\ClassMethod $function, @@ -266,13 +285,17 @@ class MethodAnalyzer extends FunctionLikeAnalyzer return; } - $cased_method_name = $method_storage->cased_name; + if ($method_storage->cased_name === null) { + return; + } + + $method_name_lc = strtolower($method_storage->cased_name); $methodsOfInterest = ['__clone', '__construct', '__destruct']; - if (in_array($cased_method_name, $methodsOfInterest)) { + if (in_array($method_name_lc, $methodsOfInterest, true)) { IssueBuffer::maybeAdd( new MethodSignatureMustOmitReturnType( - 'Method ' . $cased_method_name . ' must not declare a return type', + 'Method ' . $method_storage->cased_name . ' must not declare a return type', $code_location ) ); @@ -288,4 +311,20 @@ class MethodAnalyzer extends FunctionLikeAnalyzer strtolower($function_name) ); } + + public static function checkForbiddenEnumMethod(MethodStorage $method_storage): void + { + if ($method_storage->cased_name === null || $method_storage->location === null) { + return; + } + + $method_name_lc = strtolower($method_storage->cased_name); + if (in_array($method_name_lc, self::FORBIDDEN_ENUM_METHODS, true)) { + IssueBuffer::maybeAdd(new InvalidEnumMethod( + 'Enums cannot define ' . $method_storage->cased_name, + $method_storage->location, + $method_storage->defining_fqcln . '::' . $method_storage->cased_name + )); + } + } } diff --git a/src/Psalm/Issue/InvalidEnumMethod.php b/src/Psalm/Issue/InvalidEnumMethod.php new file mode 100644 index 000000000..7b0d35581 --- /dev/null +++ b/src/Psalm/Issue/InvalidEnumMethod.php @@ -0,0 +1,9 @@ + [], 'php_version' => '8.1', ], + 'forbiddenMethod' => [ + 'code' => ' 'InvalidEnumMethod', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } }