Skip to content

Commit

Permalink
[TypeDeclaration] Add AddReturnArrayDocblockBasedOnArrayMapRector
Browse files Browse the repository at this point in the history
  • Loading branch information
TomasVotruba committed Aug 14, 2024
1 parent e09d82c commit 0e6cdf7
Show file tree
Hide file tree
Showing 5 changed files with 301 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\AddReturnArrayDocblockBasedOnArrayMapRector;

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

final class AddReturnArrayDocblockBasedOnArrayMapRectorTest 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,34 @@
<?php

namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\AddReturnArrayDocblockBasedOnArrayMapRector\Fixture;

final class FunctionArrayMapReturn
{
public function __construct(array $items)
{
return array_map(function ($item): int {
return $item;
}, $items);
}
}

?>
-----
<?php

namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\AddReturnArrayDocblockBasedOnArrayMapRector\Fixture;

final class FunctionArrayMapReturn
{
/**
* @return int[]
*/
public function __construct(array $items)
{
return array_map(function ($item): int {
return $item;
}, $items);
}
}

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

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\TypeDeclaration\Rector\ClassMethod\AddReturnArrayDocblockBasedOnArrayMapRector;

return RectorConfig::configure()
->withRules([AddReturnArrayDocblockBasedOnArrayMapRector::class]);
52 changes: 52 additions & 0 deletions rules/TypeDeclaration/PhpDocParser/ReturnPhpDocDecorator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace Rector\TypeDeclaration\PhpDocParser;

use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Function_;
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
use PHPStan\Type\ArrayType;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
use Rector\Comments\NodeDocBlock\DocBlockUpdater;
use Rector\StaticTypeMapper\StaticTypeMapper;

final readonly class ReturnPhpDocDecorator
{
public function __construct(
private PhpDocInfoFactory $phpDocInfoFactory,
private StaticTypeMapper $staticTypeMapper,
private DocBlockUpdater $docBlockUpdater,
) {
}

public function decorateWithArray(ArrayType $arrayType, ClassMethod|Function_ $functionLike): bool
{
// private function updateFunctionLikeReturnDocBlock(
// array $closureReturnTypes,
// ClassMethod|Function_ $functionLike
// ): null|Function_|ClassMethod {
// if (count($closureReturnTypes) !== 1) {
// return null;
// }

// easy return
$functionLikePhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($functionLike);

// has already filled @return?
if ($functionLikePhpDocInfo->getReturnTagValue() instanceof ReturnTagValueNode) {
// @todo extend for mixed/dummy array
return false;
}

// $closureReturnType = $closureReturnTypes[0];

$phpDocReturnTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPHPStanPhpDocTypeNode($arrayType);
$functionLikePhpDocInfo->addTagValueNode(new ReturnTagValueNode($phpDocReturnTypeNode, ''));

$this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($functionLike);

return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
<?php

declare(strict_types=1);

namespace Rector\TypeDeclaration\Rector\ClassMethod;

use PhpParser\Node;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Function_;
use PHPStan\Type\ArrayType;
use PHPStan\Type\MixedType;
use Rector\PhpParser\Node\BetterNodeFinder;
use Rector\Rector\AbstractRector;
use Rector\StaticTypeMapper\StaticTypeMapper;
use Rector\TypeDeclaration\NodeAnalyzer\ReturnAnalyzer;
use Rector\TypeDeclaration\PhpDocParser\ReturnPhpDocDecorator;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @see \Rector\Tests\TypeDeclaration\Rector\ClassMethod\AddReturnArrayDocblockBasedOnArrayMapRector\AddReturnArrayDocblockBasedOnArrayMapRectorTest
*/
final class AddReturnArrayDocblockBasedOnArrayMapRector extends AbstractRector
{
public function __construct(
private readonly BetterNodeFinder $betterNodeFinder,
private readonly ReturnAnalyzer $returnAnalyzer,
private readonly StaticTypeMapper $staticTypeMapper,
private readonly ReturnPhpDocDecorator $returnPhpDocDecorator,
) {
}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Add @return array docblock based on array_map() return strict type',
[
new CodeSample(
<<<'CODE_SAMPLE'
class SomeClass
{
public function getItems(array $items)
{
return array_map(function ($item): int {
return $item->id;
}, $items);
}
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
class SomeClass
{
/**
* @return int[]
*/
public function getItems(array $items)
{
return array_map(function ($item): int {
return $item->id;
}, $items);
}
}
CODE_SAMPLE
)],
);
}

public function getNodeTypes(): array
{
return [ClassMethod::class, Function_::class];
}

/**
* @param ClassMethod|Function_ $node
*/
public function refactor(Node $node): null|Function_|ClassMethod
{
$returnsScoped = $this->betterNodeFinder->findReturnsScoped($node);

if ($this->hasNonArrayReturnType($node)) {
return null;
}

// nothing to return? skip it
if ($returnsScoped === []) {
return null;
}

// only returns with expr and no void
if (! $this->returnAnalyzer->hasOnlyReturnWithExpr($node, $returnsScoped)) {
return null;
}

$closureReturnTypes = [];

foreach ($returnsScoped as $returnScoped) {
if (! $returnScoped->expr instanceof FuncCall) {
return null;
}

$funcCall = $returnScoped->expr;
if (! $this->isName($funcCall, 'array_map')) {
return null;
}

// lets infer strict array_map() type
$firstArg = $funcCall->getArgs()[0];
if (! $firstArg->value instanceof Closure) {
return null;
}

$arrayMapClosure = $firstArg->value;
if (! $arrayMapClosure->returnType instanceof Node) {
return null;
}

$closureReturnTypes[] = $this->staticTypeMapper->mapPhpParserNodePHPStanType($arrayMapClosure->returnType);
}

if (count($closureReturnTypes) !== 1) {
// sole type for now
return null;
}

$arrayType = new ArrayType(new MixedType(), $closureReturnTypes[0]);

if (! $this->returnPhpDocDecorator->decorateWithArray($arrayType, $node)) {
return null;
}

return $node;
}

private function hasNonArrayReturnType(ClassMethod|Function_ $functionLike): bool
{
if (! $functionLike->returnType instanceof Identifier) {
return false;
}

return $functionLike->returnType->toLowerString() !== 'array';
}

// /**
// * @param Type[] $closureReturnTypes
// */
// private function updateFunctionLikeReturnDocBlock(
// array $closureReturnTypes,
// ClassMethod|Function_ $functionLike
// ): null|Function_|ClassMethod {
// if (count($closureReturnTypes) !== 1) {
// return null;
// }
//
// // easy return
// $functionLikePhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($functionLike);
//
// // has already filled @return?
// if ($functionLikePhpDocInfo->getReturnTagValue() instanceof ReturnTagValueNode) {
// // @todo extend for mixed/dummy array
// return null;
// }
//
// $closureReturnType = $closureReturnTypes[0];
//
// $phpDocReturnTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPHPStanPhpDocTypeNode(
// new ArrayType(new MixedType(), $closureReturnType)
// );
// $functionLikePhpDocInfo->addTagValueNode(new ReturnTagValueNode($phpDocReturnTypeNode, ''));
//
// $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($functionLike);
//
// return $functionLike;
// }
}

0 comments on commit 0e6cdf7

Please sign in to comment.