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

Type hint array reduce closure #6725

Merged
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,28 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\TypeDeclaration\Rector\FunctionLike\AddClosureParamTypeForArrayReduceRector;

use Iterator;
use PHPUnit\Framework\Attributes\DataProvider;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;

final class AddClosureParamTypeForArrayReduceRectorTest extends AbstractRectorTestCase
{
#[DataProvider('provideData')]
public function test(string $filePath): void
{
$this->doTestFile($filePath);
}

public static function provideData(): Iterator
{
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
}

public function provideConfigFilePath(): string
{
return __DIR__ . '/config/configured_rule.php';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace Rector\Tests\TypeDeclaration\Rector\FunctionLike\AddClosureParamTypeForArrayReduceRector\Fixture;

class Fixture
{
/**
* @param list<string> $array
*/
public function run(array $array)
{
return array_reduce($array, function ($carry, $value) {
return $carry . $value;
}, '');
}

/**
* @param list<string|int> $array
*/
public function runTwo(array $array)
{
return array_reduce($array, function ($carry, $value) {
return $carry . $value;
}, 100);
}
}

?>
-----
<?php

namespace Rector\Tests\TypeDeclaration\Rector\FunctionLike\AddClosureParamTypeForArrayReduceRector\Fixture;

class Fixture
{
/**
* @param list<string> $array
*/
public function run(array $array)
{
return array_reduce($array, function (string $carry, string $value) {
return $carry . $value;
}, '');
}

/**
* @param list<string|int> $array
*/
public function runTwo(array $array)
{
return array_reduce($array, function (int|string $carry, int|string $value) {
return $carry . $value;
}, 100);
}
}

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

namespace Rector\Tests\TypeDeclaration\Rector\FunctionLike\AddClosureParamTypeForArrayReduceRector\Fixture;

class SkipMixedType
{
/**
* @param array<int, mixed> $array
*/
public function run(array $array)
{
return array_reduce($array, function ($carry, $value) {
return $value->foo($carry);
}, '');
}

/**
* @param array<int, mixed> $array
*/
public function runTwo(array $array, mixed $initial)
{
return array_reduce($array, function ($carry, $value) {
return '';
}, $initial);
}
}

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

namespace Rector\Tests\TypeDeclaration\Rector\FunctionLike\AddClosureParamTypeForArrayMapRector\Fixture;

function array_reduce(array $array, callable $func, $initial = null): array
{

}

class SkipNonArrayReduceFunctions
{
/**
* @param array<int, string> $array
*/
public function run(array $array)
{
return \Rector\Tests\TypeDeclaration\Rector\FunctionLike\AddClosureParamTypeForArrayMapRector\Fixture\array_reduce($array, function ($value, $key) {
return $value . $key;
});
}
}

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

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\TypeDeclaration\Rector\FunctionLike\AddClosureParamTypeForArrayReduceRector;
use Rector\ValueObject\PhpVersionFeature;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig
->rules([AddClosureParamTypeForArrayReduceRector::class]);

$rectorConfig->phpVersion(PhpVersionFeature::UNION_TYPES);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<?php

declare(strict_types=1);

namespace Rector\TypeDeclaration\Rector\FunctionLike;

use PhpParser\Node;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Param;
use PHPStan\Reflection\Native\NativeFunctionReflection;
use PHPStan\Type\ClosureType;
use PHPStan\Type\MixedType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use PHPStan\Type\UnionTypeHelper;
use Rector\NodeTypeResolver\TypeComparator\TypeComparator;
use Rector\PHPStanStaticTypeMapper\Enum\TypeKind;
use Rector\Rector\AbstractRector;
use Rector\Reflection\ReflectionResolver;
use Rector\StaticTypeMapper\StaticTypeMapper;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @see \Rector\Tests\TypeDeclaration\Rector\FunctionLike\AddClosureParamTypeForArrayReduceRector\AddClosureParamTypeForArrayReduceRectorTest
*/
final class AddClosureParamTypeForArrayReduceRector extends AbstractRector
{
public function __construct(
private readonly TypeComparator $typeComparator,
private readonly StaticTypeMapper $staticTypeMapper,
private readonly ReflectionResolver $reflectionResolver,
) {
}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Applies type hints to array_map closures',
[
new CodeSample(
<<<'CODE_SAMPLE'
array_reduce($strings, function ($carry, $value, $key): string {
return $carry . $value;
}, $initialString);
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
array_reduce($strings, function (string $carry, string $value): string {
return $carry . $value;
}, $initialString);
CODE_SAMPLE
,
),
]
);
}

public function getNodeTypes(): array
{
return [FuncCall::class];
}

/**
* @param FuncCall $node
*/
public function refactor(Node $node): ?Node
{
if ($node->isFirstClassCallable()) {
return null;
}

if (! $this->isName($node, 'array_reduce')) {
return null;
}

$funcReflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($node);

if (! $funcReflection instanceof NativeFunctionReflection) {
return null;
}

$args = $node->getArgs();

if (! isset($args[1]) || ! $args[1]->value instanceof Closure) {
return null;
}

$closureType = $this->getType($args[1]->value);
if (! $closureType instanceof ClosureType) {
return null;
}

$carryType = $closureType->getReturnType();

if (isset($args[2])) {
$carryType = $this->combineTypes([$this->getType($args[2]->value), $carryType]);
}

$type = $this->getType($args[0]->value);
$valueType = $type->getIterableValueType();

if ($this->updateClosureWithTypes($args[1]->value, $valueType, $carryType)) {
return $node;
}

return null;
}

private function updateClosureWithTypes(Closure $closure, ?Type $valueType, ?Type $carryType): bool
{
$changes = false;
$carryParam = $closure->params[0] ?? null;
$valueParam = $closure->params[1] ?? null;

if ($valueParam instanceof Param && $valueType instanceof Type && $this->refactorParameter(
$valueParam,
$valueType
)) {
$changes = true;
}

if ($carryParam instanceof Param && $carryType instanceof Type && $this->refactorParameter(
$carryParam,
$carryType
)) {
return true;
}

return $changes;
}

private function refactorParameter(Param $param, Type $type): bool
{
if ($type instanceof MixedType) {
return false;
}

// already set → no change
if ($param->type instanceof Node) {
$currentParamType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($param->type);
if ($this->typeComparator->areTypesEqual($currentParamType, $type)) {
return false;
}
}

$paramTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($type, TypeKind::PARAM);

if (! $paramTypeNode instanceof Node) {
return false;
}

$param->type = $paramTypeNode;

return true;
}

/**
* @param Type[] $types
*/
private function combineTypes(array $types): ?Type
{
if ($types === []) {
return null;
}

$types = array_reduce($types, function (array $types, Type $type): array {
foreach ($types as $previousType) {
if ($this->typeComparator->areTypesEqual($type, $previousType)) {
return $types;
}
}

$types[] = $type;
return $types;
}, []);

if (count($types) === 1) {
return $types[0];
}

return new UnionType(UnionTypeHelper::sortTypes($types));
}
}
4 changes: 4 additions & 0 deletions src/Config/Level/TypeDeclarationLevel.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
use Rector\TypeDeclaration\Rector\Closure\ClosureReturnTypeRector;
use Rector\TypeDeclaration\Rector\Empty_\EmptyOnNullableObjectToInstanceOfRector;
use Rector\TypeDeclaration\Rector\Function_\AddFunctionVoidReturnTypeWhereNoReturnRector;
use Rector\TypeDeclaration\Rector\FunctionLike\AddClosureParamTypeForArrayMapRector;
use Rector\TypeDeclaration\Rector\FunctionLike\AddClosureParamTypeForArrayReduceRector;
use Rector\TypeDeclaration\Rector\FunctionLike\AddParamTypeSplFixedArrayRector;
use Rector\TypeDeclaration\Rector\FunctionLike\AddReturnTypeDeclarationFromYieldsRector;
use Rector\TypeDeclaration\Rector\Property\TypedPropertyFromAssignsRector;
Expand Down Expand Up @@ -120,6 +122,8 @@ final class TypeDeclarationLevel
// closures
AddClosureNeverReturnTypeRector::class,
ClosureReturnTypeRector::class,
AddClosureParamTypeForArrayReduceRector::class,
AddClosureParamTypeForArrayMapRector::class,

// more risky rules
ReturnTypeFromStrictParamRector::class,
Expand Down