Skip to content

Commit

Permalink
WIP introduced Psalm templates with ability to restrict possible valu…
Browse files Browse the repository at this point in the history
…es on type level
  • Loading branch information
thunderer committed Jan 22, 2024
1 parent 795e681 commit f47fc3d
Show file tree
Hide file tree
Showing 14 changed files with 122 additions and 14 deletions.
10 changes: 9 additions & 1 deletion psalm.xml
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
<?xml version="1.0"?>
<psalm
errorLevel="1"
findUnusedCode="false"
resolveFromConfigFile="true"
findUnusedBaselineEntry="true"
findUnusedPsalmSuppress="false"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd">

<projectFiles>
<directory name="src" />
<directory name="tests/Psalm" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
Expand All @@ -30,6 +33,11 @@
<file name="src/Doctrine/PlatenumDoctrineType.php" />
</errorLevel>
</DeprecatedMethod>
<ImpureByReferenceAssignment errorLevel="error">
<errorLevel type="suppress">
<file name="src/Enum/EnumTrait.php" />
</errorLevel>
</ImpureByReferenceAssignment>
</issueHandlers>

</psalm>
10 changes: 7 additions & 3 deletions src/Doctrine/PlatenumDoctrineType.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
use Doctrine\DBAL\Types\Type;
use Thunder\Platenum\Enum\EnumTrait;

/** @psalm-suppress PropertyNotSetInConstructor, MissingConstructor */
/**
* @psalm-suppress PropertyNotSetInConstructor, MissingConstructor
* @psalm-external-mutation-free
*/
final class PlatenumDoctrineType extends Type
{
/** @var class-string */
Expand Down Expand Up @@ -107,6 +110,7 @@ public function getName(): string

public function getSQLDeclaration(array $column, AbstractPlatform $platform): string
{
/** @psalm-suppress ImpureFunctionCall */
return ($this->platenumSql)($column, $platform);
}

Expand All @@ -120,7 +124,7 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform)
throw new \LogicException(sprintf($message, self::class, gettype($value)));
}

/** @psalm-suppress MixedMethodCall */
/** @psalm-suppress MixedMethodCall,ImpureFunctionCall */
return ($this->platenumCallback)($value->getValue());
}

Expand All @@ -130,7 +134,7 @@ public function convertToPHPValue($value, AbstractPlatform $platform)
return null;
}

/** @psalm-suppress MixedMethodCall */
/** @psalm-suppress MixedMethodCall,ImpureFunctionCall */
return ($this->platenumClass)::fromValue(($this->platenumCallback)($value));
}

Expand Down
3 changes: 3 additions & 0 deletions src/Enum/AbstractCallbackEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

/**
* @author Tomasz Kowalczyk <[email protected]>
* @psalm-template T
* @psalm-immutable
*/
abstract class AbstractCallbackEnum implements \JsonSerializable
{
/** @use CallbackEnumTrait<T> */
use CallbackEnumTrait;
}
3 changes: 3 additions & 0 deletions src/Enum/AbstractConstantsEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

/**
* @author Tomasz Kowalczyk <[email protected]>
* @psalm-template T
* @psalm-immutable
*/
abstract class AbstractConstantsEnum implements \JsonSerializable
{
/** @use ConstantsEnumTrait<T> */
use ConstantsEnumTrait;
}
3 changes: 3 additions & 0 deletions src/Enum/AbstractDocblockEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

/**
* @author Tomasz Kowalczyk <[email protected]>
* @psalm-template T
* @psalm-immutable
*/
abstract class AbstractDocblockEnum implements \JsonSerializable
{
/** @use DocblockEnumTrait<T> */
use DocblockEnumTrait;
}
3 changes: 3 additions & 0 deletions src/Enum/AbstractStaticEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

/**
* @author Tomasz Kowalczyk <[email protected]>
* @psalm-template T
* @psalm-immutable
*/
abstract class AbstractStaticEnum implements \JsonSerializable
{
/** @use StaticEnumTrait<T> */
use StaticEnumTrait;

/** @var array */
Expand Down
3 changes: 3 additions & 0 deletions src/Enum/CallbackEnumTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@

/**
* @author Tomasz Kowalczyk <[email protected]>
* @psalm-template T
* @psalm-immutable
*/
trait CallbackEnumTrait
{
/** @use EnumTrait<T> */
use EnumTrait;

/** @var non-empty-array<class-string,callable():array<string,int|string>> */
Expand Down
3 changes: 3 additions & 0 deletions src/Enum/ConstantsEnumTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

/**
* @author Tomasz Kowalczyk <[email protected]>
* @psalm-template T
* @psalm-immutable
*/
trait ConstantsEnumTrait
{
/** @use EnumTrait<T> */
use EnumTrait;

private static function resolve(): array
Expand Down
3 changes: 3 additions & 0 deletions src/Enum/DocblockEnumTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@

/**
* @author Tomasz Kowalczyk <[email protected]>
* @psalm-template T
* @psalm-immutable
*/
trait DocblockEnumTrait
{
/** @use EnumTrait<T> */
use EnumTrait;

private static function resolve(): array
Expand Down
37 changes: 27 additions & 10 deletions src/Enum/EnumTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,22 @@

/**
* @author Tomasz Kowalczyk <[email protected]>
* @psalm-template T
* @psalm-immutable
*/
trait EnumTrait
{
/** @var string */
private $member;
/** @var int|string */
/** @psalm-var T */

Check failure on line 16 in src/Enum/EnumTrait.php

View workflow job for this annotation

GitHub Actions / test (7.4, ubuntu-latest, false)

UndefinedDocblockClass

src/Enum/EnumTrait.php:16:20: UndefinedDocblockClass: Docblock-defined class, interface or enum named Thunder\Platenum\Enum\EnumTrait does not exist (see https://psalm.dev/200)

Check failure on line 16 in src/Enum/EnumTrait.php

View workflow job for this annotation

GitHub Actions / test (8.0, ubuntu-latest, false)

UndefinedDocblockClass

src/Enum/EnumTrait.php:16:20: UndefinedDocblockClass: Docblock-defined class, interface or enum named Thunder\Platenum\Enum\EnumTrait does not exist (see https://psalm.dev/200)

Check failure on line 16 in src/Enum/EnumTrait.php

View workflow job for this annotation

GitHub Actions / test (8.1, ubuntu-latest, false)

UndefinedDocblockClass

src/Enum/EnumTrait.php:16:20: UndefinedDocblockClass: Docblock-defined class, interface or enum named Thunder\Platenum\Enum\EnumTrait does not exist (see https://psalm.dev/200)

Check failure on line 16 in src/Enum/EnumTrait.php

View workflow job for this annotation

GitHub Actions / test (8.2, ubuntu-latest, false)

UndefinedDocblockClass

src/Enum/EnumTrait.php:16:20: UndefinedDocblockClass: Docblock-defined class, interface or enum named Thunder\Platenum\Enum\EnumTrait does not exist (see https://psalm.dev/200)

Check failure on line 16 in src/Enum/EnumTrait.php

View workflow job for this annotation

GitHub Actions / test (8.3, ubuntu-latest, false)

UndefinedDocblockClass

src/Enum/EnumTrait.php:16:20: UndefinedDocblockClass: Docblock-defined class, interface or enum named Thunder\Platenum\Enum\EnumTrait does not exist (see https://psalm.dev/200)
private $value;

/** @var non-empty-array<string,non-empty-array<string,int|string>> */
/** @psalm-var non-empty-array<string,non-empty-array<string,int|string>> */
protected static $members = [];
/** @var array<string,array<string,static>> */
protected static $instances = [];

/** @param int|string $value */
/** @psalm-param T $value */
/* final */ private function __construct(string $member, $value)
{
$this->member = $member;
Expand Down Expand Up @@ -51,8 +53,18 @@ final public static function fromMember(string $member): self
static::throwDefaultInvalidMemberException($member);
}

/** @psalm-suppress PropertyTypeCoercion */
return static::$instances[$class][$member] = static::fromMemberAndValue($member, static::$members[$class][$member]);
}

/**
* @psalm-param int|string $value
* @psalm-return static
*/
/* final */ private static function fromMemberAndValue(string $member, $value): self
{
/** @psalm-suppress UnsafeInstantiation */
return static::$instances[$class][$member] = new static($member, static::$members[$class][$member]);
return new static($member, $value);
}

/**
Expand Down Expand Up @@ -91,12 +103,15 @@ final public static function fromEnum($enum): self
}

/**
* @param static $enum
* @param-out AbstractConstantsEnum|AbstractDocblockEnum|AbstractStaticEnum|AbstractCallbackEnum|AbstractAttributeEnum $enum
* @param self &$enum
* @param-out self $enum
* @psalm-suppress ReferenceConstraintViolation
*/
final public function fromInstance(&$enum): void
{
$enum = static::fromEnum($enum);
/** @psalm-suppress ImpureMethodCall,ArgumentTypeCoercion */
$instance = static::fromEnum($enum);
$enum = $instance;
}

/**
Expand Down Expand Up @@ -154,12 +169,13 @@ final public function getMember(): string
return $this->member;
}

/** @return int|string */
/** @psalm-return T */
final public function getValue()
{
return $this->value;
}

/** @psalm-return T */
#[\ReturnTypeWillChange]
final public function jsonSerialize()
{
Expand Down Expand Up @@ -209,7 +225,7 @@ final public function hasMember(string $members): bool
return $members === $this->member;
}

/** @param int|string $value */
/** @psalm-param T $value */
final public function hasValue($value): bool
{
return $value === $this->value;
Expand Down Expand Up @@ -293,7 +309,7 @@ private static function resolveMembers(): void
// reflection instead of method_exists because of PHP 7.4 bug #78632
// @see https://bugs.php.net/bug.php?id=78632
$hasResolve = (new \ReflectionClass($class))->hasMethod('resolve');
/** @var array<string,int|string> $members */
/** @psalm-var array<string,T> $members */
$members = $hasResolve ? static::resolve() : $throwMissingResolve($class);
if(empty($members)) {
throw PlatenumException::fromEmptyMembers($class);
Expand All @@ -308,6 +324,7 @@ private static function resolveMembers(): void
throw PlatenumException::fromNonUniformMemberValues($class, $members);
}

/** @psalm-suppress MixedPropertyTypeCoercion */
static::$members[$class] = $members;
}

Expand Down
3 changes: 3 additions & 0 deletions src/Enum/StaticEnumTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@

/**
* @author Tomasz Kowalczyk <[email protected]>
* @psalm-template T
* @psalm-immutable
*/
trait StaticEnumTrait
{
/** @use EnumTrait<T> */
use EnumTrait;

private static function resolve(): array
Expand Down
18 changes: 18 additions & 0 deletions tests/Psalm/PsalmConstantsExtendsEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Thunder\Platenum\Tests\Psalm;

use Thunder\Platenum\Enum\AbstractConstantsEnum;

/**
* @method static static FIRST()
* @method static static SECOND()
* @template-extends AbstractConstantsEnum<self::FIRST|self::SECOND>
* @psalm-immutable
* @psalm-suppress PropertyNotSetInConstructor
*/
final class PsalmConstantsExtendsEnum extends AbstractConstantsEnum

Check failure on line 14 in tests/Psalm/PsalmConstantsExtendsEnum.php

View workflow job for this annotation

GitHub Actions / test (7.4, ubuntu-latest, false)

UndefinedDocblockClass

tests/Psalm/PsalmConstantsExtendsEnum.php:14:13: UndefinedDocblockClass: Docblock-defined class, interface or enum named PsalmConstantsExtendsEnum does not exist (see https://psalm.dev/200)

Check failure on line 14 in tests/Psalm/PsalmConstantsExtendsEnum.php

View workflow job for this annotation

GitHub Actions / test (8.0, ubuntu-latest, false)

UndefinedDocblockClass

tests/Psalm/PsalmConstantsExtendsEnum.php:14:13: UndefinedDocblockClass: Docblock-defined class, interface or enum named PsalmConstantsExtendsEnum does not exist (see https://psalm.dev/200)

Check failure on line 14 in tests/Psalm/PsalmConstantsExtendsEnum.php

View workflow job for this annotation

GitHub Actions / test (8.1, ubuntu-latest, false)

UndefinedDocblockClass

tests/Psalm/PsalmConstantsExtendsEnum.php:14:13: UndefinedDocblockClass: Docblock-defined class, interface or enum named PsalmConstantsExtendsEnum does not exist (see https://psalm.dev/200)

Check failure on line 14 in tests/Psalm/PsalmConstantsExtendsEnum.php

View workflow job for this annotation

GitHub Actions / test (8.2, ubuntu-latest, false)

UndefinedDocblockClass

tests/Psalm/PsalmConstantsExtendsEnum.php:14:13: UndefinedDocblockClass: Docblock-defined class, interface or enum named PsalmConstantsExtendsEnum does not exist (see https://psalm.dev/200)

Check failure on line 14 in tests/Psalm/PsalmConstantsExtendsEnum.php

View workflow job for this annotation

GitHub Actions / test (8.3, ubuntu-latest, false)

UndefinedDocblockClass

tests/Psalm/PsalmConstantsExtendsEnum.php:14:13: UndefinedDocblockClass: Docblock-defined class, interface or enum named PsalmConstantsExtendsEnum does not exist (see https://psalm.dev/200)
{
public const FIRST = 1;
public const SECOND = 2;
}
20 changes: 20 additions & 0 deletions tests/Psalm/PsalmConstantsTraitEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Thunder\Platenum\Tests\Psalm;

use Thunder\Platenum\Enum\ConstantsEnumTrait;

/**
* @method static static FIRST()
* @method static static SECOND()
* @psalm-template T self::FIRST|self::SECOND
* @psalm-immutable
*/
final class PsalmConstantsTraitEnum
{
/** @use ConstantsEnumTrait<T> */
use ConstantsEnumTrait;

public const FIRST = 1;
public const SECOND = 2;
}
17 changes: 17 additions & 0 deletions tests/Psalm/PsalmEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Thunder\Platenum\Tests\Psalm;

final class PsalmEnum
{
}

PsalmConstantsTraitEnum::FIRST();
PsalmConstantsTraitEnum::SECOND();
PsalmConstantsTraitEnum::fromMember('THIRD');
PsalmConstantsTraitEnum::fromValue(4);

PsalmConstantsExtendsEnum::FIRST();
PsalmConstantsExtendsEnum::SECOND();
PsalmConstantsExtendsEnum::fromMember('THIRD');
PsalmConstantsExtendsEnum::fromValue(4);

0 comments on commit f47fc3d

Please sign in to comment.