Skip to content

Commit

Permalink
add full generic collection type
Browse files Browse the repository at this point in the history
  • Loading branch information
TomasVotruba committed Aug 15, 2024
1 parent b531e2b commit cf948f4
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class VarAndAttribute
private $trainings = [];

/**
* @param Collection|\Rector\Doctrine\Tests\CodeQuality\Rector\Property\ImproveDoctrineCollectionDocTypeInEntityRector\Source\Training[] $trainings
* @param \Doctrine\Common\Collections\Collection<int, \Rector\Doctrine\Tests\CodeQuality\Rector\Property\ImproveDoctrineCollectionDocTypeInEntityRector\Source\Training> $trainings
*/
public function setTrainings($trainings)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public function transform(EntityMapping $entityMapping, Property|Param $property
unset($oneToManyMapping[EntityMappingKey::ORDER_BY]);

$args = $this->nodeFactory->createArgs($oneToManyMapping);
NodeValueNormalizer::ensureKeyIsClassConstFetch($args, 'targetEntity');
NodeValueNormalizer::ensureKeyIsClassConstFetch($args, EntityMappingKey::TARGET_ENTITY);

$property->attrGroups[] = AttributeFactory::createGroup($this->getClassName(), $args);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Property;
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
use PHPStan\Type\Type;
use PHPStan\Type\Generic\GenericObjectType;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTypeChanger;
use Rector\Doctrine\CodeQuality\Enum\CollectionMapping;
use Rector\Doctrine\CodeQuality\Enum\EntityMappingKey;
use Rector\Doctrine\CodeQuality\SetterCollectionResolver;
use Rector\Doctrine\NodeAnalyzer\AttributeFinder;
use Rector\Doctrine\NodeAnalyzer\TargetEntityResolver;
Expand Down Expand Up @@ -119,7 +120,7 @@ private function refactorProperty(Property $property): ?Property
$targetEntityExpr = $this->attributeFinder->findAttributeByClassesArgByName(
$property,
CollectionMapping::TO_MANY_CLASSES,
'targetEntity'
EntityMappingKey::TARGET_ENTITY
);

if (! $targetEntityExpr instanceof Expr) {
Expand All @@ -141,8 +142,11 @@ private function refactorClassMethod(Class_ $class): ?Class_
continue;
}

$collectionObjectType = $this->setterCollectionResolver->resolveAssignedType($class, $classMethod);
if (! $collectionObjectType instanceof Type) {
$collectionObjectType = $this->setterCollectionResolver->resolveAssignedGenericCollectionType(
$class,
$classMethod
);
if (! $collectionObjectType instanceof GenericObjectType) {
continue;
}

Expand All @@ -159,6 +163,7 @@ private function refactorClassMethod(Class_ $class): ?Class_

/** @var string $parameterName */
$parameterName = $this->getName($param);

$this->phpDocTypeChanger->changeParamType(
$classMethod,
$phpDocInfo,
Expand Down
55 changes: 53 additions & 2 deletions rules/CodeQuality/SetterCollectionResolver.php
Original file line number Diff line number Diff line change
@@ -1,31 +1,45 @@
<?php

declare(strict_types=1);

namespace Rector\Doctrine\CodeQuality;

use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Property;
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
use PHPStan\Reflection\Php\PhpPropertyReflection;
use PHPStan\Type\ArrayType;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use Rector\Doctrine\TypeAnalyzer\CollectionTypeFactory;
use Rector\Doctrine\TypeAnalyzer\CollectionVarTagValueNodeResolver;
use Rector\NodeManipulator\AssignManipulator;
use Rector\NodeNameResolver\NodeNameResolver;
use Rector\Reflection\ReflectionResolver;
use Rector\StaticTypeMapper\StaticTypeMapper;
use Rector\StaticTypeMapper\ValueObject\Type\ShortenedObjectType;

final readonly class SetterCollectionResolver
{
/**
* @var string
*/
private const COLLECTION_CLASS = 'Doctrine\Common\Collections\Collection';

public function __construct(
private AssignManipulator $assignManipulator,
private ReflectionResolver $reflectionResolver,
private NodeNameResolver $nodeNameResolver,
private CollectionVarTagValueNodeResolver $collectionVarTagValueNodeResolver,
private StaticTypeMapper $staticTypeMapper,
private CollectionTypeFactory $collectionTypeFactory
) {
}

public function resolveAssignedType(Class_ $class, ClassMethod $classMethod): ?Type
public function resolveAssignedGenericCollectionType(Class_ $class, ClassMethod $classMethod): ?GenericObjectType
{
$propertyFetches = $this->assignManipulator->resolveAssignsToLocalPropertyFetches($classMethod);
if (count($propertyFetches) !== 1) {
Expand All @@ -51,6 +65,43 @@ public function resolveAssignedType(Class_ $class, ClassMethod $classMethod): ?T
return null;
}

return $this->staticTypeMapper->mapPHPStanPhpDocTypeNodeToPHPStanType($varTagValueNode->type, $property);
// remove collection union type, so this can be turned into generic type
$resolvedType = $this->staticTypeMapper->mapPHPStanPhpDocTypeNodeToPHPStanType(
$varTagValueNode->type,
$property
);

if ($resolvedType instanceof UnionType) {
$nonCollectionTypes = [];
foreach ($resolvedType->getTypes() as $unionedType) {
if (! $this->isCollectionType($unionedType)) {
continue;
}

$nonCollectionTypes[] = $unionedType;
}

if (count($nonCollectionTypes) === 1) {
$soleType = $nonCollectionTypes[0];
if ($soleType instanceof ArrayType && $soleType->getItemType() instanceof ObjectType) {
return $this->collectionTypeFactory->createType($soleType->getItemType());
}
}
}

if ($resolvedType instanceof GenericObjectType) {
return $resolvedType;
}

return null;
}

private function isCollectionType(Type $type): bool
{
if ($type instanceof ShortenedObjectType && $type->getFullyQualifiedName() === self::COLLECTION_CLASS) {
return true;
}

return $type instanceof ObjectType && $type->getClassName() === self::COLLECTION_CLASS;
}
}
3 changes: 2 additions & 1 deletion src/NodeAnalyzer/TargetEntityResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use PhpParser\Node\Identifier;
use PhpParser\Node\Scalar\String_;
use PHPStan\Reflection\ReflectionProvider;
use Rector\Doctrine\CodeQuality\Enum\EntityMappingKey;
use Rector\Exception\NotImplementedYetException;
use Rector\NodeNameResolver\NodeNameResolver;

Expand All @@ -28,7 +29,7 @@ public function resolveFromAttribute(Attribute $attribute): ?string
continue;
}

if ($arg->name->toString() !== 'targetEntity') {
if ($arg->name->toString() !== EntityMappingKey::TARGET_ENTITY) {
continue;
}

Expand Down
6 changes: 3 additions & 3 deletions src/TypeAnalyzer/CollectionTypeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@

use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\IntegerType;
use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType;
use PHPStan\Type\ObjectType;

final class CollectionTypeFactory
{
public function createType(FullyQualifiedObjectType $fullyQualifiedObjectType): GenericObjectType
public function createType(ObjectType $objectType): GenericObjectType
{
$genericTypes = [new IntegerType(), $fullyQualifiedObjectType];
$genericTypes = [new IntegerType(), $objectType];
return new GenericObjectType('Doctrine\Common\Collections\Collection', $genericTypes);
}
}

0 comments on commit cf948f4

Please sign in to comment.