├── phpstan.neon.dist ├── extension.neon ├── composer.json ├── src ├── EnumMethodsClassReflectionExtension.php ├── EnumMethodReflection.php └── EnumDynamicReturnTypeExtension.php ├── README.md └── LICENSE.txt /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - extension.neon 3 | 4 | parameters: 5 | level: 5 6 | paths: 7 | - src 8 | - tests 9 | excludePaths: 10 | - tests/assets 11 | - tests/integration/data 12 | -------------------------------------------------------------------------------- /extension.neon: -------------------------------------------------------------------------------- 1 | services: 2 | - class: MabeEnumPHPStan\EnumMethodsClassReflectionExtension 3 | tags: 4 | - phpstan.broker.methodsClassReflectionExtension 5 | 6 | - class: MabeEnumPHPStan\EnumDynamicReturnTypeExtension 7 | tags: 8 | - phpstan.broker.dynamicStaticMethodReturnTypeExtension 9 | - phpstan.broker.dynamicMethodReturnTypeExtension 10 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "marc-mabe/php-enum-phpstan", 3 | "description": "Enum class reflection extension for PHPStan", 4 | "type": "phpstan-extension", 5 | "keywords": ["enum", "phpstan"], 6 | "license": "BSD-3-Clause", 7 | "require": { 8 | "php": "^7.4 | ^8.0", 9 | "marc-mabe/php-enum": "^4.0", 10 | "phpstan/phpstan": "^2.0" 11 | }, 12 | "require-dev": { 13 | "phpunit/phpunit": "^9.6" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "MabeEnumPHPStan\\": "src/" 18 | } 19 | }, 20 | "autoload-dev": { 21 | "psr-4": { 22 | "MabeEnum\\PHPStan\\tests\\": "tests/" 23 | } 24 | }, 25 | "extra": { 26 | "phpstan": { 27 | "includes": [ 28 | "extension.neon" 29 | ] 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/EnumMethodsClassReflectionExtension.php: -------------------------------------------------------------------------------- 1 | isSubclassOf(Enum::class)) { 15 | return false; 16 | } 17 | 18 | $class = $classReflection->getName(); 19 | return array_key_exists($methodName, $class::getConstants()); 20 | } 21 | 22 | public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection 23 | { 24 | return new EnumMethodReflection($classReflection, $methodName); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHPStan extension for php-enum 2 | [![License](https://poser.pugx.org/marc-mabe/php-enum-phpstan/license)](https://github.com/marc-mabe/php-enum-phpstan/blob/master/LICENSE.txt) 3 | [![Latest Stable](https://img.shields.io/packagist/v/marc-mabe/php-enum-phpstan)](https://packagist.org/packages/marc-mabe/php-enum-phpstan) 4 | 5 | [PHP-Enum](https://github.com/marc-mabe/php-enum) enumerations with native PHP. 6 | 7 | [PHPStan](https://phpstan.org/) is a static code analysis tool. 8 | 9 | > PHPStan focuses on finding errors in your code without actually running it. 10 | > It catches whole classes of bugs even before you write tests for the code. 11 | > It moves PHP closer to compiled languages in the sense that the correctness 12 | > of each line of the code can be checked before you run the actual line. 13 | 14 | This PHPStan extension makes enumerator accessor methods and enum possible values known to PHPStan. 15 | 16 | ## Install 17 | 18 | Install via [Composer](https://getcomposer.org) 19 | 20 | ``` 21 | composer require --dev marc-mabe/php-enum-phpstan 22 | ``` 23 | 24 | and include extension.neon in your project's PHPStan config 25 | 26 | ``` 27 | includes: 28 | - vendor/marc-mabe/php-enum-phpstan/extension.neon 29 | ``` 30 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, Marc Bennewitz 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the organisation nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /src/EnumMethodReflection.php: -------------------------------------------------------------------------------- 1 | classReflection = $classReflection; 23 | $this->name = $name; 24 | } 25 | 26 | public function getName(): string 27 | { 28 | return $this->name; 29 | } 30 | 31 | public function getDeclaringClass(): ClassReflection 32 | { 33 | return $this->classReflection; 34 | } 35 | 36 | public function isStatic(): bool 37 | { 38 | return true; 39 | } 40 | 41 | public function isPrivate(): bool 42 | { 43 | return false; 44 | } 45 | 46 | public function isPublic(): bool 47 | { 48 | return true; 49 | } 50 | 51 | public function getPrototype(): ClassMemberReflection 52 | { 53 | return $this; 54 | } 55 | 56 | public function getVariants(): array 57 | { 58 | return [ 59 | new FunctionVariant( 60 | TemplateTypeMap::createEmpty(), 61 | TemplateTypeMap::createEmpty(), 62 | [], 63 | false, 64 | new ObjectType($this->classReflection->getName()) 65 | ), 66 | ]; 67 | } 68 | 69 | public function getDocComment(): ?string 70 | { 71 | $docComment = $this->classReflection->getConstant($this->name)->getDocComment(); 72 | 73 | if ($docComment) { 74 | // remove @var annotation of constant definition 75 | $docComment = preg_replace('/@var.*/', '', $docComment); 76 | } 77 | 78 | return $docComment; 79 | } 80 | 81 | public function isDeprecated(): TrinaryLogic 82 | { 83 | return $this->classReflection->getConstant($this->name)->isDeprecated(); 84 | } 85 | 86 | public function getDeprecatedDescription(): ?string 87 | { 88 | return $this->classReflection->getConstant($this->name)->getDeprecatedDescription(); 89 | } 90 | 91 | public function isFinal(): TrinaryLogic 92 | { 93 | return TrinaryLogic::createNo(); 94 | } 95 | 96 | public function isInternal(): TrinaryLogic 97 | { 98 | return TrinaryLogic::createNo(); 99 | } 100 | 101 | public function getThrowType(): ?Type 102 | { 103 | return null; 104 | } 105 | 106 | public function hasSideEffects(): TrinaryLogic 107 | { 108 | return TrinaryLogic::createNo(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/EnumDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | private array $objectMethods = []; 27 | 28 | /** 29 | * Map supported static method to a callable function detecting return type 30 | * 31 | * @var array 32 | */ 33 | private array $staticMethods = []; 34 | 35 | /** 36 | * Buffer of all types of enumeration values 37 | * @phpstan-var array, Type[]> 38 | */ 39 | private array $enumValueTypesBuffer = []; 40 | 41 | /** 42 | * Buffer of all types of enumeration ordinals 43 | * @phpstan-var array, Type[]> 44 | */ 45 | private array $enumOrdinalTypesBuffer = []; 46 | 47 | public function __construct() 48 | { 49 | $this->objectMethods['getvalue'] = function (string $class) { 50 | return $this->detectGetValueReturnType($class); 51 | }; 52 | 53 | $this->staticMethods['getvalues'] = function (string $class) { 54 | return $this->detectGetValuesReturnType($class); 55 | }; 56 | 57 | // static methods can be called like object methods 58 | $this->objectMethods = array_merge($this->objectMethods, $this->staticMethods); 59 | } 60 | 61 | public function getClass(): string 62 | { 63 | return Enum::class; 64 | } 65 | 66 | public function isStaticMethodSupported(MethodReflection $methodReflection): bool 67 | { 68 | $methodLower = strtolower($methodReflection->getName()); 69 | return array_key_exists($methodLower, $this->staticMethods); 70 | } 71 | 72 | public function isMethodSupported(MethodReflection $methodReflection): bool 73 | { 74 | $methodLower = strtolower($methodReflection->getName()); 75 | return array_key_exists($methodLower, $this->objectMethods); 76 | } 77 | 78 | public function getTypeFromStaticMethodCall( 79 | MethodReflection $methodReflection, 80 | StaticCall $staticCall, 81 | Scope $scope 82 | ): Type { 83 | $callClass = $staticCall->class; 84 | 85 | // The call class is not a name 86 | // E.g. an expression on $enumClass::getValues() 87 | if (!$callClass instanceof Name) { 88 | return ParametersAcceptorSelector::selectFromArgs( 89 | $scope, 90 | $staticCall->getArgs(), 91 | $methodReflection->getVariants() 92 | )->getReturnType(); 93 | } 94 | 95 | $callClassName = $callClass->toString(); 96 | 97 | // Can't detect possible types on static::*() 98 | // as it depends on defined enumerators of unknown inherited classes 99 | if ($callClassName === 'static') { 100 | return ParametersAcceptorSelector::selectFromArgs( 101 | $scope, 102 | $staticCall->getArgs(), 103 | $methodReflection->getVariants() 104 | )->getReturnType(); 105 | } 106 | 107 | if ($callClassName === 'self') { 108 | $callClassName = $scope->getClassReflection()->getName(); 109 | } 110 | 111 | $methodLower = strtolower($methodReflection->getName()); 112 | return $this->staticMethods[$methodLower]($callClassName); 113 | } 114 | 115 | public function getTypeFromMethodCall( 116 | MethodReflection $methodReflection, 117 | MethodCall $methodCall, 118 | Scope $scope 119 | ): Type { 120 | $methodLower = strtolower($methodReflection->getName()); 121 | $returnTypes = []; 122 | foreach ($scope->getType($methodCall->var)->getReferencedClasses() as $callClass) { 123 | $returnTypes[] = $this->objectMethods[$methodLower]($callClass); 124 | } 125 | 126 | return TypeCombinator::union(...$returnTypes); 127 | } 128 | 129 | /** 130 | * Returns types of all values of an enumeration 131 | * @phpstan-param class-string $enumeration 132 | * @return Type[] 133 | */ 134 | private function enumValueTypes(string $enumeration): array 135 | { 136 | if (isset($this->enumValueTypesBuffer[$enumeration])) { 137 | return $this->enumValueTypesBuffer[$enumeration]; 138 | } 139 | 140 | $values = array_values($enumeration::getConstants()); 141 | $types = array_map([ConstantTypeHelper::class, 'getTypeFromValue'], $values); 142 | 143 | return $this->enumValueTypesBuffer[$enumeration] = $types; 144 | } 145 | 146 | /** 147 | * Returns types of all ordinals of an enumeration 148 | * @phpstan-param class-string $enumeration 149 | * @return Type[] 150 | */ 151 | private function enumOrdinalTypes(string $enumeration): array 152 | { 153 | if (isset($this->enumOrdinalTypesBuffer[$enumeration])) { 154 | return $this->enumOrdinalTypesBuffer[$enumeration]; 155 | } 156 | 157 | $ordinals = array_keys($enumeration::getOrdinals()); 158 | $types = array_map([ConstantTypeHelper::class, 'getTypeFromValue'], $ordinals); 159 | 160 | return $this->enumOrdinalTypesBuffer[$enumeration] = $types; 161 | } 162 | 163 | /** 164 | * Returns return type of Enum::getValue() 165 | * @phpstan-param class-string $enumeration 166 | */ 167 | private function detectGetValueReturnType(string $enumeration): Type 168 | { 169 | return TypeCombinator::union(...$this->enumValueTypes($enumeration)); 170 | } 171 | 172 | /** 173 | * Returns return type of Enum::getValues() 174 | * @phpstan-param class-string $enumeration 175 | */ 176 | private function detectGetValuesReturnType(string $enumeration): Type 177 | { 178 | $keyTypes = $this->enumOrdinalTypes($enumeration); 179 | $valueTypes = $this->enumValueTypes($enumeration); 180 | 181 | $builder = ConstantArrayTypeBuilder::createEmpty(); 182 | foreach ($keyTypes as $i => $keyType) { 183 | $valueType = $valueTypes[$i]; 184 | $builder->setOffsetValueType($keyType, $valueType); 185 | } 186 | 187 | return $builder->getArray(); 188 | } 189 | } 190 | --------------------------------------------------------------------------------