diff --git a/composer.json b/composer.json index 924f924..6d37f2f 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,6 @@ "require-dev": { "phpunit/phpunit": "^9.5", "squizlabs/php_codesniffer": "1.*", - "vimeo/psalm": "^4.6.2" + "vimeo/psalm": "4.6.2" } } diff --git a/phpunit.xml b/phpunit.xml index bd714f1..13b4060 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,4 +10,9 @@ ./tests + + + src + + diff --git a/psalm.xml b/psalm.xml index ff06b66..24ed533 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,10 +1,11 @@ @@ -16,8 +17,6 @@ - - diff --git a/src/Enum.php b/src/Enum.php index 6967ab5..5f8b068 100644 --- a/src/Enum.php +++ b/src/Enum.php @@ -93,9 +93,10 @@ public function __wakeup() } /** + * @psalm-pure * @param mixed $value * @return static - * @psalm-return static + * @psalm-return static */ public static function from($value): self { @@ -127,7 +128,6 @@ public function getKey() /** * @psalm-pure - * @psalm-suppress InvalidCast * @return string */ public function __toString() @@ -187,7 +187,6 @@ public static function values() * Returns all possible values as an array * * @psalm-pure - * @psalm-suppress ImpureStaticProperty * * @psalm-return array * @return array Constant name in key, constant value in value @@ -212,8 +211,9 @@ public static function toArray() * @param $value * @psalm-param mixed $value * @psalm-pure - * @psalm-assert-if-true T $value * @return bool + * + * deprecated use {@see Enum::isValidEnumValue()} instead */ public static function isValid($value) { @@ -224,8 +224,23 @@ public static function isValid($value) * Asserts valid enum value * * @psalm-pure - * @psalm-assert T $value + * @psalm-template CheckedValueType + * @psalm-param class-string> $enumType * @param mixed $value + * @psalm-assert-if-true CheckedValueType $value + */ + public static function isValidEnumValue(string $enumType, $value): bool + { + return $enumType::isValid($value); + } + + /** + * Asserts valid enum value + * + * @psalm-pure + * @param mixed $value + * + * deprecated use {@see Enum::assertValidEnumValue()} instead */ public static function assertValidValue($value): void { @@ -236,7 +251,20 @@ public static function assertValidValue($value): void * Asserts valid enum value * * @psalm-pure - * @psalm-assert T $value + * @psalm-template ValueType + * @psalm-param class-string> $enumType + * @param mixed $value + * @psalm-assert ValueType $value + */ + public static function assertValidEnumValue(string $enumType, $value): void + { + $enumType::assertValidValue($value); + } + + /** + * Asserts valid enum value + * + * @psalm-pure * @param mixed $value * @return string */ diff --git a/static-analysis/EnumInstantiation.php b/static-analysis/EnumInstantiation.php new file mode 100644 index 0000000..322cca8 --- /dev/null +++ b/static-analysis/EnumInstantiation.php @@ -0,0 +1,48 @@ + + */ +final class InstantiatedEnum extends Enum +{ + const A = 'A'; + const C = 'C'; +} + +/** + * @psalm-pure + * @psalm-return InstantiatedEnum<'A'> + */ +function canCallConstructorWithConstantValue(): InstantiatedEnum +{ + return new InstantiatedEnum('A'); +} + +/** + * @psalm-pure + * @psalm-return InstantiatedEnum<'C'> + */ +function canCallConstructorWithConstantReference(): InstantiatedEnum +{ + return new InstantiatedEnum(InstantiatedEnum::C); +} + +/** @psalm-pure */ +function canCallFromWithKnownValue(): InstantiatedEnum +{ + return InstantiatedEnum::from('C'); +} + +/** @psalm-pure */ +function canCallFromWithUnknownValue(): InstantiatedEnum +{ + return InstantiatedEnum::from(123123); +} diff --git a/static-analysis/EnumIsPure.php b/static-analysis/EnumIsPure.php index 5875fd8..696694e 100644 --- a/static-analysis/EnumIsPure.php +++ b/static-analysis/EnumIsPure.php @@ -11,7 +11,7 @@ * @method static PureEnum C() * * @psalm-immutable - * @psalm-template T of 'A'|'B' + * @psalm-template T of 'A'|'C' * @template-extends Enum */ final class PureEnum extends Enum diff --git a/static-analysis/EnumValidation.php b/static-analysis/EnumValidation.php new file mode 100644 index 0000000..c6d7ba0 --- /dev/null +++ b/static-analysis/EnumValidation.php @@ -0,0 +1,83 @@ + + */ +final class ValidationEnum extends Enum +{ + const A = 'A'; + const C = 'C'; +} + +/** + * @psalm-pure + * @param mixed $input + * @psalm-return 'A'|'C' + * + * @psalm-suppress MixedReturnStatement + * @psalm-suppress MixedInferredReturnType at the time of this writing, we did not yet find + * a proper approach to constraint input values through + * validation via static methods. + */ +function canValidateValue($input): string +{ + ValidationEnum::assertValidValue($input); + + return $input; +} + +/** + * @psalm-pure + * @param mixed $input + * @psalm-return 'A'|'C' + */ +function canAssertValidEnumValue($input): string +{ + ValidationEnum::assertValidEnumValue(ValidationEnum::class, $input); + + return $input; +} + +/** + * @psalm-pure + * @param mixed $input + * @psalm-return 'A'|'C' + * + * @psalm-suppress MixedReturnStatement + * @psalm-suppress MixedInferredReturnType at the time of this writing, we did not yet find + * a proper approach to constraint input values through + * validation via static methods. + */ +function canValidateValueThroughIsValid($input): string +{ + if (! ValidationEnum::isValid($input)) { + throw new \InvalidArgumentException('Value not valid'); + } + + return $input; +} + +/** + * @psalm-pure + * @param mixed $input + * @psalm-return 'A'|'C'|1 + * + * @psalm-suppress InvalidReturnType https://github.com/vimeo/psalm/issues/5372 + * @psalm-suppress InvalidReturnStatement https://github.com/vimeo/psalm/issues/5372 + */ +function canValidateValueThroughIsValidEnumValue($input) +{ + if (! ValidationEnum::isValidEnumValue(ValidationEnum::class, $input)) { + return 1; + } + + return $input; +} diff --git a/tests/EnumTest.php b/tests/EnumTest.php index 6fcc5b1..37559ea 100755 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -6,6 +6,8 @@ namespace MyCLabs\Tests\Enum; +use MyCLabs\Enum\Enum; + /** * @author Matthieu Napoli * @author Daniel Costa @@ -59,6 +61,27 @@ public function testFailToCreateEnumWithInvalidValueThroughNamedConstructor($val EnumFixture::from($value); } + /** + * @dataProvider invalidValueProvider + * @param mixed $value + */ + public function testRejectInvalidEnumValueWhenAssertingOnIt($value): void + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('is not part of the enum MyCLabs\Tests\Enum\EnumFixture'); + + Enum::assertValidEnumValue(EnumFixture::class, $value); + } + + /** + * @dataProvider invalidValueProvider + * @param mixed $value + */ + public function testReportsFalseOnNonValidEnumValueUponChecking($value): void + { + self::assertFalse(Enum::isValidEnumValue(EnumFixture::class, $value)); + } + public function testFailToCreateEnumWithEnumItselfThroughNamedConstructor(): void { $this->expectException(\UnexpectedValueException::class); @@ -67,6 +90,11 @@ public function testFailToCreateEnumWithEnumItselfThroughNamedConstructor(): voi EnumFixture::from(EnumFixture::FOO()); } + public function testCreateEnumWithValidEnumValueThroughNamedConstructor(): void + { + self::assertEquals(EnumFixture::FOO(), EnumFixture::from('foo')); + } + /** * Contains values not existing in EnumFixture * @return array @@ -179,7 +207,8 @@ public function testBadStaticAccess() */ public function testIsValid($value, $isValid) { - $this->assertSame($isValid, EnumFixture::isValid($value)); + self::assertSame($isValid, EnumFixture::isValid($value)); + self::assertSame($isValid, Enum::isValidEnumValue(EnumFixture::class, $value)); } public function isValidProvider() @@ -381,4 +410,19 @@ public function testAssertValidValue($value, $isValid): void self::assertTrue(EnumFixture::isValid($value)); } + + /** + * @dataProvider isValidProvider + */ + public function testAssertValidValueWithTypedApi($value, $isValid): void + { + if (!$isValid) { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage("Value '$value' is not part of the enum " . EnumFixture::class); + } + + Enum::assertValidEnumValue(EnumFixture::class, $value); + + self::assertTrue(Enum::isValidEnumValue(EnumFixture::class, $value)); + } }