diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromJMSSerializerAttributeTypeRector/Fixture/float_var_string.php.inc b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromJMSSerializerAttributeTypeRector/Fixture/float_var_string.php.inc new file mode 100644 index 00000000000..c2ec4e75038 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromJMSSerializerAttributeTypeRector/Fixture/float_var_string.php.inc @@ -0,0 +1,30 @@ + +----- + diff --git a/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromJMSSerializerAttributeTypeRector.php b/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromJMSSerializerAttributeTypeRector.php index e8afb98afeb..8bb276b09d8 100644 --- a/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromJMSSerializerAttributeTypeRector.php +++ b/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromJMSSerializerAttributeTypeRector.php @@ -5,10 +5,9 @@ namespace Rector\TypeDeclaration\Rector\Class_; use PhpParser\Node; +use PhpParser\Node\Attribute; use PhpParser\Node\Expr; -use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Identifier; -use PhpParser\Node\Name; use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\NullableType; use PhpParser\Node\Stmt\Class_; @@ -16,7 +15,13 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; +use PHPStan\Type\StringType; +use PHPStan\Type\Type; +use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo; +use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; +use Rector\DeadCode\PhpDoc\TagRemover\VarTagRemover; use Rector\Doctrine\CodeQuality\Enum\CollectionMapping; +use Rector\Doctrine\NodeAnalyzer\AttributeFinder; use Rector\Enum\ClassName; use Rector\Php74\Guard\MakePropertyTypedGuard; use Rector\Php80\NodeAnalyzer\PhpAttributeAnalyzer; @@ -43,7 +48,10 @@ public function __construct( private readonly ValueResolver $valueResolver, private readonly PhpAttributeAnalyzer $phpAttributeAnalyzer, private readonly ScalarStringToTypeMapper $scalarStringToTypeMapper, - private readonly StaticTypeMapper $staticTypeMapper + private readonly StaticTypeMapper $staticTypeMapper, + private readonly AttributeFinder $attributeFinder, + private readonly PhpDocInfoFactory $phpDocInfoFactory, + private readonly VarTagRemover $varTagRemover ) { } @@ -90,34 +98,18 @@ public function provideMinPhpVersion(): int public function refactor(Node $node): ?Node { $hasChanged = false; - $classReflection = null; - foreach ($node->getProperties() as $property) { - if ($property->type instanceof Node || $property->props[0]->default instanceof Expr) { - continue; - } - - if (! $this->phpAttributeAnalyzer->hasPhpAttribute($property, ClassName::JMS_TYPE)) { - continue; - } - - // this will be most likely collection, not single type - if ($this->phpAttributeAnalyzer->hasPhpAttributes( - $property, - array_merge(CollectionMapping::TO_MANY_CLASSES, CollectionMapping::TO_ONE_CLASSES) - )) { - continue; - } - - if (! $classReflection instanceof ClassReflection) { - $classReflection = $this->reflectionResolver->resolveClassReflection($node); - } + if (! $this->hasAtLeastOneUntypedPropertyUsingJmsAttribute($node)) { + return null; + } - if (! $classReflection instanceof ClassReflection) { - return null; - } + $classReflection = $this->reflectionResolver->resolveClassReflection($node); + if (! $classReflection instanceof ClassReflection) { + return null; + } - if (! $this->makePropertyTypedGuard->isLegal($property, $classReflection, true)) { + foreach ($node->getProperties() as $property) { + if ($this->shouldSkipProperty($property, $classReflection)) { continue; } @@ -131,20 +123,13 @@ public function refactor(Node $node): ?Node continue; } - $type = $this->scalarStringToTypeMapper->mapScalarStringToType($typeValue); - if ($type instanceof MixedType) { - // fallback to object type - $type = new ObjectType($typeValue); - } - - $propertyType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($type, TypeKind::PROPERTY); - - if (! $propertyType instanceof Identifier && ! $propertyType instanceof FullyQualified) { - return null; + $propertyTypeNode = $this->createTypeNode($typeValue, $property); + if (! $propertyTypeNode instanceof Identifier && ! $propertyTypeNode instanceof FullyQualified) { + continue; } - $property->type = new NullableType($propertyType); - $property->props[0]->default = new ConstFetch(new Name('null')); + $property->type = new NullableType($propertyTypeNode); + $property->props[0]->default = $this->nodeFactory->createNull(); $hasChanged = true; } @@ -158,26 +143,80 @@ public function refactor(Node $node): ?Node private function resolveAttributeType(Property $property): ?string { - foreach ($property->attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $attr) { - if (! $this->isName($attr->name, ClassName::JMS_TYPE)) { - continue; - } + $jmsTypeAttribute = $this->attributeFinder->findAttributeByClass($property, ClassName::JMS_TYPE); + if (! $jmsTypeAttribute instanceof Attribute) { + return null; + } - $typeValue = $this->valueResolver->getValue($attr->args[0]->value); - if (! is_string($typeValue)) { - return null; - } + $typeValue = $this->valueResolver->getValue($jmsTypeAttribute->args[0]->value); + if (! is_string($typeValue)) { + return null; + } - if (StringUtils::isMatch($typeValue, '#DateTime\<(.*?)\>#')) { - // special case for DateTime, which is not a scalar type - return 'DateTime'; - } + if (StringUtils::isMatch($typeValue, '#DateTime\<(.*?)\>#')) { + // special case for DateTime, which is not a scalar type + return 'DateTime'; + } - return $typeValue; + return $typeValue; + } + + private function hasAtLeastOneUntypedPropertyUsingJmsAttribute(Class_ $class): bool + { + foreach ($class->getProperties() as $property) { + if ($property->type instanceof Node) { + continue; + } + + if ($this->attributeFinder->hasAttributeByClasses($property, [ClassName::JMS_TYPE])) { + return true; } } - return null; + return false; + } + + private function createTypeNode(string $typeValue, Property $property): ?Node + { + if ($typeValue === 'float') { + $propertyPhpDocInfo = $this->phpDocInfoFactory->createFromNode($property); + if ($propertyPhpDocInfo instanceof PhpDocInfo) { + // fallback to string, as most likely string representation of float + if ($propertyPhpDocInfo->getVarType() instanceof StringType) { + $this->varTagRemover->removeVarTag($property); + + return new Identifier('string'); + } + } + } + + $type = $this->scalarStringToTypeMapper->mapScalarStringToType($typeValue); + if ($type instanceof MixedType) { + // fallback to object type + $type = new ObjectType($typeValue); + } + + return $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($type, TypeKind::PROPERTY); + } + + private function shouldSkipProperty(Property $property, ClassReflection $classReflection): bool + { + if ($property->type instanceof Node || $property->props[0]->default instanceof Expr) { + return true; + } + + if (! $this->phpAttributeAnalyzer->hasPhpAttribute($property, ClassName::JMS_TYPE)) { + return true; + } + + // this will be most likely collection, not single type + if ($this->phpAttributeAnalyzer->hasPhpAttributes( + $property, + array_merge(CollectionMapping::TO_MANY_CLASSES, CollectionMapping::TO_ONE_CLASSES) + )) { + return true; + } + + return ! $this->makePropertyTypedGuard->isLegal($property, $classReflection); } }