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)); } }