Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add known type casting to AnnotationToAttributeRector #6217

Merged
merged 4 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 !== []) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on else, union with empty types is impossible, I think this can be handled by count($unionedTypesWithoutNullType) === 1

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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