Skip to content

Commit

Permalink
Add known type casting to AnnotationToAttributeRector (#6217)
Browse files Browse the repository at this point in the history
* add casting fixture

* Add known type casting to AnnotationToAttributeRector

* make generic

* add validation file with union string or int
  • Loading branch information
TomasVotruba authored Aug 6, 2024
1 parent 0f09b7a commit 147c961
Show file tree
Hide file tree
Showing 13 changed files with 274 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\Symfony;

use Symfony\Component\Validator\Constraints as Assert;

final class ValidationCastIntegerParameter
{
/**
* @Assert\Length(
* min="100",
* max="255"
* )
*/
public function action()
{
}
}

?>
-----
<?php

namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\Symfony;

use Symfony\Component\Validator\Constraints as Assert;

final class ValidationCastIntegerParameter
{
#[Assert\Length(min: 100, max: 255)]
public function action()
{
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\Symfony;

use Symfony\Component\Validator\Constraints as Assert;

final class ValidationFile
{
/**
* @Assert\File(maxSize="100")
*/
public function action()
{
}
}

?>
-----
<?php

namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\Symfony;

use Symfony\Component\Validator\Constraints as Assert;

final class ValidationFile
{
#[Assert\File(maxSize: 100)]
public function action()
{
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\Symfony;

use Symfony\Component\Validator\Constraints as Assert;

final class ValidationFileWithString
{
/**
* @Assert\File(maxSize="5555K")
*/
public function action()
{
}
}

?>
-----
<?php

namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\Symfony;

use Symfony\Component\Validator\Constraints as Assert;

final class ValidationFileWithString
{
#[Assert\File(maxSize: '5555K')]
public function action()
{
}
}

?>
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use Symfony\Component\Validator\Constraints as Assert;

final class ValidationIntegerParameter
{
#[Assert\Length(min: '100', max: '255', maxMessage: 'some Message', allowed: 'true')]
#[Assert\Length(min: 100, max: 255, maxMessage: 'some Message', allowed: 'true')]
public function action()
{
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
// validation
new AnnotationToAttribute('Symfony\Component\Validator\Constraints\Choice'),
new AnnotationToAttribute('Symfony\Component\Validator\Constraints\Length'),
new AnnotationToAttribute('Symfony\Component\Validator\Constraints\File'),

// JMS + Symfony
new AnnotationToAttribute('Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter'),
Expand Down
5 changes: 4 additions & 1 deletion rules/Php71/Rector/TryCatch/MultiExceptionCatchRector.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ public function refactor(Node $node): ?Node
// use current var as next var
$node->catches[$key + 1]->var = $node->catches[$key]->var;
// merge next types as current merge to next types
$node->catches[$key + 1]->types = array_merge($node->catches[$key]->types, $node->catches[$key + 1]->types);
$node->catches[$key + 1]->types = array_merge(
$node->catches[$key]->types,
$node->catches[$key + 1]->types
);

unset($node->catches[$key]);
$hasChanged = true;
Expand Down
6 changes: 5 additions & 1 deletion src/PHPStanStaticTypeMapper/Utils/TypeUnwrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public function unwrapFirstObjectTypeFromUnionType(Type $type): Type
return $type;
}

public function removeNullTypeFromUnionType(UnionType $unionType): UnionType
public function removeNullTypeFromUnionType(UnionType $unionType): Type
{
$unionedTypesWithoutNullType = [];

Expand All @@ -39,6 +39,10 @@ public function removeNullTypeFromUnionType(UnionType $unionType): UnionType
$unionedTypesWithoutNullType[] = $type;
}

if ($unionedTypesWithoutNullType !== []) {
return $unionedTypesWithoutNullType[0];
}

return new UnionType($unionedTypesWithoutNullType);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

declare(strict_types=1);

namespace Rector\PhpAttribute\NodeFactory;

use PhpParser\Node\Arg;
use PhpParser\Node\Expr\ArrayItem;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Scalar\String_;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\IntegerType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use Rector\Php80\ValueObject\AnnotationToAttribute;
use Webmozart\Assert\Assert;

final readonly class AnnotationToAttributeIntegerValueCaster
{
public function __construct(
private ReflectionProvider $reflectionProvider,
) {
}

/**
* @param Arg[] $args
*/
public function castAttributeTypes(AnnotationToAttribute $annotationToAttribute, array $args): void
{
Assert::allIsInstanceOf($args, Arg::class);

if (! $this->reflectionProvider->hasClass($annotationToAttribute->getAttributeClass())) {
return;
}

$attributeClassReflection = $this->reflectionProvider->getClass($annotationToAttribute->getAttributeClass());
if (! $attributeClassReflection->hasConstructor()) {
return;
}

$parameterReflections = $this->resolveConstructorParameterReflections($attributeClassReflection);

foreach ($parameterReflections as $parameterReflection) {
foreach ($args as $arg) {
if (! $arg->value instanceof ArrayItem) {
continue;
}

$arrayItem = $arg->value;
if (! $arrayItem->key instanceof String_) {
continue;
}

$keyString = $arrayItem->key;
if ($keyString->value !== $parameterReflection->getName()) {
continue;
}

// ensure type is casted to integer
if (! $arrayItem->value instanceof String_) {
continue;
}

if (! $this->containsInteger($parameterReflection->getType())) {
continue;
}

$valueString = $arrayItem->value;
if (! is_numeric($valueString->value)) {
continue;
}

$arrayItem->value = new LNumber((int) $valueString->value);
}
}
}

private function containsInteger(Type $type): bool
{
if ($type instanceof IntegerType) {
return true;
}

if (! $type instanceof UnionType) {
return false;
}

foreach ($type->getTypes() as $unionedType) {
if ($unionedType instanceof IntegerType) {
return true;
}
}

return false;
}

/**
* @return ParameterReflection[]
*/
private function resolveConstructorParameterReflections(ClassReflection $classReflection): array
{
$extendedMethodReflection = $classReflection->getConstructor();

$parametersAcceptorWithPhpDocs = ParametersAcceptorSelector::selectSingle(
$extendedMethodReflection->getVariants()
);

return $parametersAcceptorWithPhpDocs->getParameters();
}
}
21 changes: 12 additions & 9 deletions src/PhpAttribute/NodeFactory/PhpAttributeGroupFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public function __construct(
private AnnotationToAttributeMapper $annotationToAttributeMapper,
private AttributeNameFactory $attributeNameFactory,
private NamedArgsFactory $namedArgsFactory,
private AttributeArrayNameInliner $attributeArrayNameInliner
private AttributeArrayNameInliner $attributeArrayNameInliner,
private AnnotationToAttributeIntegerValueCaster $annotationToAttributeIntegerValueCaster,
) {
}

Expand All @@ -57,8 +58,8 @@ public function createFromClass(string $attributeClass): AttributeGroup
public function createFromClassWithItems(string $attributeClass, array $items): AttributeGroup
{
$fullyQualified = new FullyQualified($attributeClass);
$args = $this->createArgsFromItems($items, $attributeClass);

$args = $this->createArgsFromItems($items);
$attribute = new Attribute($fullyQualified, $args);

return new AttributeGroup([$attribute]);
Expand All @@ -73,11 +74,9 @@ public function create(
array $uses
): AttributeGroup {
$values = $doctrineAnnotationTagValueNode->getValuesWithSilentKey();
$args = $this->createArgsFromItems(
$values,
$annotationToAttribute->getAttributeClass(),
$annotationToAttribute->getClassReferenceFields()
);
$args = $this->createArgsFromItems($values, '', $annotationToAttribute->getClassReferenceFields());

$this->annotationToAttributeIntegerValueCaster->castAttributeTypes($annotationToAttribute, $args);

$args = $this->attributeArrayNameInliner->inlineArrayToArgs($args);

Expand Down Expand Up @@ -105,10 +104,14 @@ public function create(
*
* @param ArrayItemNode[]|mixed[] $items
* @param string[] $classReferencedFields
*
* @return Arg[]
*/
public function createArgsFromItems(array $items, string $attributeClass, array $classReferencedFields = []): array
{
public function createArgsFromItems(
array $items,
string $attributeClass = '',
array $classReferencedFields = []
): array {
$mappedItems = $this->annotationToAttributeMapper->map($items);

$this->mapClassReferences($mappedItems, $classReferencedFields);
Expand Down
17 changes: 17 additions & 0 deletions stubs/Symfony/Component/Validator/Constraints/File.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Symfony\Component\Validator\Constraints;

if (class_exists('Symfony\Component\Validator\Constraints\File')) {
return;
}

// @see https://github.com/symfony/validator/blob/94e7465b1271ba024bd96a424da037e3390184a5/Constraints/File.php

class File
{
public function __construct(
int|string|null $maxSize = null
) {
}
}
18 changes: 18 additions & 0 deletions stubs/Symfony/Component/Validator/Constraints/Length.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Symfony\Component\Validator\Constraints;

if (class_exists('Symfony\Component\Validator\Constraints\Length')) {
return;
}

// @see https://github.com/symfony/validator/blob/94e7465b1271ba024bd96a424da037e3390184a5/Constraints/Length.php

class Length
{
public function __construct(
?int $min,
?int $max
) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public function testCreateArgsFromItems(): void
$args = $this->phpAttributeGroupFactory->createArgsFromItems([
new ArrayItemNode(new StringNode('/path'), 'path'),
new ArrayItemNode(new StringNode('action'), 'name'),
], 'SomeClass');
]);

$this->assertCount(2, $args);
$this->assertContainsOnlyInstancesOf(Arg::class, $args);
Expand Down

0 comments on commit 147c961

Please sign in to comment.