Valinor/tests/Unit/Definition/Repository/Reflection/ReflectionClassDefinitionRepositoryTest.php
Daniil Gentili afcedf9e56
feat: handle literal boolean true / false types
Allows the usage of boolean values, as follows:

```php
class Foo
{
    /** @var int|false */
    public readonly int|bool $value;
}
```
2022-05-09 21:14:46 +02:00

347 lines
13 KiB
PHP

<?php
declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Unit\Definition\Repository\Reflection;
use CuyZ\Valinor\Definition\Exception\ClassTypeAliasesDuplication;
use CuyZ\Valinor\Definition\Exception\InvalidParameterDefaultValue;
use CuyZ\Valinor\Definition\Exception\InvalidPropertyDefaultValue;
use CuyZ\Valinor\Definition\Exception\InvalidTypeAliasImportClass;
use CuyZ\Valinor\Definition\Exception\InvalidTypeAliasImportClassType;
use CuyZ\Valinor\Definition\Exception\TypesDoNotMatch;
use CuyZ\Valinor\Definition\Exception\UnknownTypeAliasImport;
use CuyZ\Valinor\Definition\Repository\Reflection\ReflectionClassDefinitionRepository;
use CuyZ\Valinor\Tests\Fake\Definition\Repository\FakeAttributesRepository;
use CuyZ\Valinor\Tests\Fake\Type\FakeType;
use CuyZ\Valinor\Tests\Fake\Type\Parser\Factory\FakeTypeParserFactory;
use CuyZ\Valinor\Type\StringType;
use CuyZ\Valinor\Type\Types\NativeBooleanType;
use CuyZ\Valinor\Type\Types\ClassType;
use CuyZ\Valinor\Type\Types\MixedType;
use CuyZ\Valinor\Type\Types\UnresolvableType;
use PHPUnit\Framework\TestCase;
use function get_class;
final class ReflectionClassDefinitionRepositoryTest extends TestCase
{
private ReflectionClassDefinitionRepository $repository;
protected function setUp(): void
{
parent::setUp();
$this->repository = new ReflectionClassDefinitionRepository(
new FakeTypeParserFactory(),
new FakeAttributesRepository(),
);
}
public function test_properties_can_be_retrieved(): void
{
$object = new class () {
public string $propertyWithDefaultValue = 'Default value for property';
/** @var bool */
public $propertyWithDocBlockType;
public $propertyWithNoType; // @phpstan-ignore-line
public string $publicProperty;
protected string $protectedProperty;
private bool $privateProperty; // @phpstan-ignore-line
};
$type = new ClassType(get_class($object));
$properties = $this->repository->for($type)->properties();
self::assertTrue($properties->get('propertyWithDefaultValue')->hasDefaultValue());
self::assertSame('Default value for property', $properties->get('propertyWithDefaultValue')->defaultValue());
self::assertInstanceOf(NativeBooleanType::class, $properties->get('propertyWithDocBlockType')->type());
self::assertInstanceOf(MixedType::class, $properties->get('propertyWithNoType')->type());
self::assertInstanceOf(StringType::class, $properties->get('publicProperty')->type());
self::assertTrue($properties->get('publicProperty')->isPublic());
self::assertInstanceOf(StringType::class, $properties->get('protectedProperty')->type());
self::assertFalse($properties->get('protectedProperty')->isPublic());
self::assertInstanceOf(NativeBooleanType::class, $properties->get('privateProperty')->type());
self::assertFalse($properties->get('privateProperty')->isPublic());
}
public function test_methods_can_be_retrieved(): void
{
$object = new class () {
public function __construct()
{
}
/**
* @param string $parameterWithDocBlockType
* @phpstan-ignore-next-line
*/
public function publicMethod(
bool $mandatoryParameter,
$parameterWithNoType,
$parameterWithDocBlockType,
string $optionalParameter = 'Optional parameter value'
): void {
}
public function publicMethodWithReturnType(): string
{
return 'foo';
}
/**
* @return string
*/
public function publicMethodWithDocBlockReturnType()
{
return 'foo';
}
public function publicMethodWithNativeAndDocBlockReturnTypes(): string
{
return 'foo';
}
};
$className = get_class($object);
$type = new ClassType($className);
$methods = $this->repository->for($type)->methods();
self::assertTrue($methods->hasConstructor());
self::assertInstanceOf(StringType::class, $methods->get('publicMethodWithReturnType')->returnType());
self::assertInstanceOf(StringType::class, $methods->get('publicMethodWithDocBlockReturnType')->returnType());
self::assertInstanceOf(StringType::class, $methods->get('publicMethodWithNativeAndDocBlockReturnTypes')->returnType());
$parameters = $methods->get('publicMethod')->parameters();
self::assertTrue($parameters->has('mandatoryParameter'));
self::assertTrue($parameters->has('parameterWithNoType'));
self::assertTrue($parameters->has('parameterWithDocBlockType'));
self::assertTrue($parameters->has('optionalParameter'));
$mandatoryParameter = $parameters->get('mandatoryParameter');
$parameterWithNoType = $parameters->get('parameterWithNoType');
$parameterWithDocBlockType = $parameters->get('parameterWithDocBlockType');
$optionalParameter = $parameters->get('optionalParameter');
self::assertSame($className . '::publicMethod($mandatoryParameter)', $mandatoryParameter->signature());
self::assertSame($className . '::publicMethod($parameterWithNoType)', $parameterWithNoType->signature());
self::assertSame($className . '::publicMethod($parameterWithDocBlockType)', $parameterWithDocBlockType->signature());
self::assertSame($className . '::publicMethod($optionalParameter)', $optionalParameter->signature());
self::assertInstanceOf(NativeBooleanType::class, $mandatoryParameter->type());
self::assertFalse($mandatoryParameter->isOptional());
self::assertInstanceOf(MixedType::class, $parameterWithNoType->type());
self::assertInstanceOf(StringType::class, $parameterWithDocBlockType->type());
self::assertTrue($parameters->get('optionalParameter')->isOptional());
self::assertSame('Optional parameter value', $optionalParameter->defaultValue());
}
public function test_invalid_property_type_throws_exception(): void
{
$class = get_class(new class () {
/** @var InvalidType */
public $propertyWithInvalidType; // @phpstan-ignore-line
});
$class = $this->repository->for(new ClassType($class));
$type = $class->properties()->get('propertyWithInvalidType')->type();
self::assertInstanceOf(UnresolvableType::class, $type);
/** @var UnresolvableType $type */
self::assertMatchesRegularExpression('/^The type `InvalidType` for property `.*` could not be resolved: .*$/', $type->getMessage());
}
public function test_invalid_property_default_value_throws_exception(): void
{
$class = get_class(new class () {
/** @var string */
public $propertyWithInvalidDefaultValue = false; // @phpstan-ignore-line
});
$this->expectException(InvalidPropertyDefaultValue::class);
$this->expectExceptionCode(1629211093);
$this->expectExceptionMessage("Default value of property `$class::\$propertyWithInvalidDefaultValue` is not accepted by `string`.");
$this->repository->for(new ClassType($class));
}
public function test_property_with_non_matching_types_throws_exception(): void
{
$class = get_class(new class () {
/**
* @phpstan-ignore-next-line
* @var string
*/
public bool $propertyWithNotMatchingTypes;
});
$this->expectException(TypesDoNotMatch::class);
$this->expectExceptionCode(1638471381);
$this->expectExceptionMessage("Types for property `$class::\$propertyWithNotMatchingTypes` do not match: `string` (docblock) does not accept `bool` (native).");
$this->repository->for(new ClassType($class));
}
public function test_invalid_parameter_type_throws_exception(): void
{
$class = get_class(new class () {
/**
* @formatter:off
* @param InvalidTypeWithPendingSpaces $parameterWithInvalidType
* @formatter:on
* @phpstan-ignore-next-line
*/
public function publicMethod($parameterWithInvalidType): void
{
}
});
$class = $this->repository->for(new ClassType($class));
$type = $class->methods()->get('publicMethod')->parameters()->get('parameterWithInvalidType')->type();
self::assertInstanceOf(UnresolvableType::class, $type);
/** @var UnresolvableType $type */
self::assertMatchesRegularExpression('/^The type `InvalidTypeWithPendingSpaces` for parameter `.*` could not be resolved: .*$/', $type->getMessage());
}
public function test_invalid_parameter_default_value_throws_exception(): void
{
$class = get_class(new class () {
/**
* @param string $parameterWithInvalidDefaultValue
* @phpstan-ignore-next-line
*/
public function publicMethod($parameterWithInvalidDefaultValue = false): void
{
}
});
$this->expectException(InvalidParameterDefaultValue::class);
$this->expectExceptionCode(1629210903);
$this->expectExceptionMessage("Default value of parameter `$class::publicMethod(\$parameterWithInvalidDefaultValue)` is not accepted by `string`.");
$this->repository->for(new ClassType($class));
}
public function test_parameter_with_non_matching_types_throws_exception(): void
{
$class = get_class(new class () {
/**
* @param string $parameterWithNotMatchingTypes
* @phpstan-ignore-next-line
*/
public function publicMethod(bool $parameterWithNotMatchingTypes): void
{
}
});
$this->expectException(TypesDoNotMatch::class);
$this->expectExceptionCode(1638471381);
$this->expectExceptionMessage("Types for parameter `$class::publicMethod(\$parameterWithNotMatchingTypes)` do not match: `string` (docblock) does not accept `bool` (native).");
$this->repository->for(new ClassType($class));
}
public function test_method_with_non_matching_return_types_throws_exception(): void
{
$class = get_class(new class () {
/**
* @return bool
* @phpstan-ignore-next-line
*/
public function publicMethod(): string
{
return 'foo';
}
});
$this->expectException(TypesDoNotMatch::class);
$this->expectExceptionCode(1638471381);
$this->expectExceptionMessage("Return types for method `$class::publicMethod()` do not match: `bool` (docblock) does not accept `string` (native).");
$this->repository->for(new ClassType($class));
}
public function test_class_with_local_type_alias_name_duplication_throws_exception(): void
{
$class = get_class(
/**
* @template T
* @template AnotherTemplate
* @psalm-type T = int
* @phpstan-type AnotherTemplate = int
*/
new class () { }
);
$this->expectException(ClassTypeAliasesDuplication::class);
$this->expectExceptionCode(1638477604);
$this->expectExceptionMessage("The following type aliases already exist in class `$class`: `T`, `AnotherTemplate`.");
$this->repository->for(new ClassType($class, ['T' => new FakeType(), 'AnotherTemplate' => new FakeType()]));
}
public function test_class_with_invalid_type_alias_import_class_throws_exception(): void
{
$class = get_class(
/**
* @phpstan-import-type T from UnknownType
*/
new class () { }
);
$this->expectException(InvalidTypeAliasImportClass::class);
$this->expectExceptionCode(1638535486);
$this->expectExceptionMessage("Cannot import a type alias from unknown class `UnknownType` in class `$class`.");
$this->repository->for(new ClassType($class));
}
public function test_class_with_invalid_type_alias_import_class_type_throws_exception(): void
{
$class = get_class(
/**
* @phpstan-import-type T from string
*/
new class () { }
);
$this->expectException(InvalidTypeAliasImportClassType::class);
$this->expectExceptionCode(1638535608);
$this->expectExceptionMessage("Importing a type alias can only be done with classes, `string` was given in class `$class`.");
$this->repository->for(new ClassType($class));
}
public function test_class_with_unknown_type_alias_import_throws_exception(): void
{
$class = get_class(
/**
* @phpstan-import-type T from stdClass
*/
new class () { }
);
$this->expectException(UnknownTypeAliasImport::class);
$this->expectExceptionCode(1638535757);
$this->expectExceptionMessage("Type alias `T` imported in `$class` could not be found in `stdClass`");
$this->repository->for(new ClassType($class));
}
}