Skip to content

Commit

Permalink
feat(type): add class_string types (#432) (#435)
Browse files Browse the repository at this point in the history
* feat(type): add class_string types (#432)
  • Loading branch information
zerkms committed Dec 15, 2023
1 parent 0aa7117 commit 15651cb
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/component/type.md
Expand Up @@ -15,6 +15,7 @@
- [array_key](./../../src/Psl/Type/array_key.php#L10)
- [backed_enum](./../../src/Psl/Type/backed_enum.php#L16)
- [bool](./../../src/Psl/Type/bool.php#L10)
- [class_string](./../../src/Psl/Type/class_string.php#L14)
- [converted](./../../src/Psl/Type/converted.php#L21)
- [dict](./../../src/Psl/Type/dict.php#L16)
- [f32](./../../src/Psl/Type/f32.php#L12)
Expand Down
1 change: 1 addition & 0 deletions src/Psl/Internal/Loader.php
Expand Up @@ -344,6 +344,7 @@ final class Loader
'Psl\\Type\\scalar' => 'Psl/Type/scalar.php',
'Psl\\Type\\shape' => 'Psl/Type/shape.php',
'Psl\\Type\\uint' => 'Psl/Type/uint.php',
'Psl\\Type\\class_string' => 'Psl/Type/class_string.php',
'Psl\\Type\\u32' => 'Psl/Type/u32.php',
'Psl\\Type\\u16' => 'Psl/Type/u16.php',
'Psl\\Type\\u8' => 'Psl/Type/u8.php',
Expand Down
76 changes: 76 additions & 0 deletions src/Psl/Type/Internal/ClassStringType.php
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace Psl\Type\Internal;

use Psl\Type\Exception\AssertException;
use Psl\Type\Exception\CoercionException;
use Psl\Type\Type;

/**
* @template T as object
*
* @extends Type<class-string<T>>
*
* @internal
*/
final class ClassStringType extends Type
{
/**
* @var class-string<T> $classname
*/
private string $classname;

/**
* @param class-string<T> $classname
*/
public function __construct(
string $classname
) {
$this->classname = $classname;
}

/**
* @psalm-assert-if-true class-string<T> $value
*/
public function matches(mixed $value): bool
{
return is_string($value) && is_a($value, $this->classname, true);
}

/**
* @throws CoercionException
*
* @return class-string<T>
*/
public function coerce(mixed $value): string
{
if (is_string($value) && is_a($value, $this->classname, true)) {
return $value;
}

throw CoercionException::withValue($value, $this->toString(), $this->getTrace());
}

/**
* @throws AssertException
*
* @return class-string<T>
*
* @psalm-assert class-string<T> $value
*/
public function assert(mixed $value): string
{
if (is_string($value) && is_a($value, $this->classname, true)) {
return $value;
}

throw AssertException::withValue($value, $this->toString(), $this->getTrace());
}

public function toString(): string
{
return 'class-string<' . $this->classname . '>';
}
}
17 changes: 17 additions & 0 deletions src/Psl/Type/class_string.php
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Psl\Type;

/**
* @template T
*
* @param class-string<T> $classname
*
* @return TypeInterface<class-string<T>>
*/
function class_string(string $classname): TypeInterface
{
return new Internal\ClassStringType($classname);
}
23 changes: 23 additions & 0 deletions tests/static-analysis/Type/class_string.php
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Psl\Tests\StaticAnalysis\Type;

use Psl;
use Psl\Type;

/**
* @param class-string<Psl\Collection\CollectionInterface> $_foo
*/
function take_collection_classname(string $_foo): void
{
}

/**
* @throws Psl\Type\Exception\AssertException
*/
function tests(): void
{
take_collection_classname(Type\class_string(Psl\Collection\CollectionInterface::class)->assert('foo'));
}
44 changes: 44 additions & 0 deletions tests/unit/Type/ClassStringTypeTest.php
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Psl\Tests\Unit\Type;

use Psl\Collection;
use Psl\Type;

final class ClassStringTypeTest extends TypeTest
{
public function getType(): Type\TypeInterface
{
return Type\class_string(Collection\CollectionInterface::class);
}

public function getValidCoercions(): iterable
{
yield [$_ = Collection\Vector::class, $_];
yield [$_ = Collection\MutableVector::class, $_];
yield [$_ = Collection\Map::class, $_];
yield [$_ = Collection\MutableMap::class, $_];
yield [$_ = Collection\MutableMapInterface::class, $_];
yield [$_ = Collection\CollectionInterface::class, $_];
}

public function getInvalidCoercions(): iterable
{
yield [null];
yield [STDIN];
yield ['UnknownClass'];
yield [$this->stringable('foo')];
yield [new class {
}];
}

public function getToStringExamples(): iterable
{
yield [Type\class_string(Collection\MapInterface::class), 'class-string<Psl\Collection\MapInterface>'];
yield [Type\class_string(Collection\VectorInterface::class), 'class-string<Psl\Collection\VectorInterface>'];
yield [Type\class_string(Collection\Vector::class), 'class-string<Psl\Collection\Vector>'];
yield [Type\class_string(Collection\Map::class), 'class-string<Psl\Collection\Map>'];
}
}

0 comments on commit 15651cb

Please sign in to comment.