Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\Php80\Rector\Class_\StringableForToStringRector\Fixture;

class ReturnEmptyStringNoReturnExpr
{
public function __toString()
{
if (rand(0, 1)) {
return;
}

return 'test';
}
}

?>
-----
<?php

declare(strict_types=1);

namespace Rector\Tests\Php80\Rector\Class_\StringableForToStringRector\Fixture;

class ReturnEmptyStringNoReturnExpr implements \Stringable
{
public function __toString(): string
{
if (rand(0, 1)) {
return '';
}

return 'test';
}
}

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

declare(strict_types=1);

namespace Rector\Tests\Php80\Rector\Class_\StringableForToStringRector\Fixture;

class ReturnVoid
{
public function __toString()
{
}
}

?>
-----
<?php

declare(strict_types=1);

namespace Rector\Tests\Php80\Rector\Class_\StringableForToStringRector\Fixture;

class ReturnVoid implements \Stringable
{
public function __toString(): string
{
return '';
}
}

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

declare(strict_types=1);

namespace Rector\Tests\Php80\Rector\Class_\StringableForToStringRector\Fixture;

class ReturnVoid2
{
public function __toString()
{
do_something();
}
}

?>
-----
<?php

declare(strict_types=1);

namespace Rector\Tests\Php80\Rector\Class_\StringableForToStringRector\Fixture;

class ReturnVoid2 implements \Stringable
{
public function __toString(): string
{
do_something();
return '';
}
}

?>
48 changes: 47 additions & 1 deletion rules/Php80/Rector/Class_/StringableForToStringRector.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,20 @@
namespace Rector\Php80\Rector\Class_;

use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Cast\String_ as CastString_;
use PhpParser\Node\Name;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Return_;
use PHPStan\Type\StringType;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\ValueObject\MethodName;
use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\FamilyTree\Reflection\FamilyRelationsAnalyzer;
use Rector\TypeDeclaration\TypeInferer\ReturnTypeInferer;
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
Expand All @@ -30,7 +36,8 @@ final class StringableForToStringRector extends AbstractRector implements MinPhp
private const STRINGABLE = 'Stringable';

public function __construct(
private readonly FamilyRelationsAnalyzer $familyRelationsAnalyzer
private readonly FamilyRelationsAnalyzer $familyRelationsAnalyzer,
private readonly ReturnTypeInferer $returnTypeInferer
) {
}

Expand Down Expand Up @@ -95,6 +102,11 @@ public function refactor(Node $node): ?Node
return null;
}

$returnType = $this->returnTypeInferer->inferFunctionLike($toStringClassMethod);
if (! $returnType instanceof StringType) {
$this->processNotStringType($toStringClassMethod);
}

// add interface
$node->implements[] = new FullyQualified(self::STRINGABLE);

Expand All @@ -106,4 +118,38 @@ public function refactor(Node $node): ?Node

return $node;
}

private function processNotStringType(ClassMethod $toStringClassMethod): void
{
$hasReturn = $this->betterNodeFinder->hasInstancesOfInFunctionLikeScoped($toStringClassMethod, Return_::class);

if (! $hasReturn) {
$lastKey = array_key_last((array) $toStringClassMethod->stmts);
Copy link
Copy Markdown
Member Author

@samsonasik samsonasik Dec 13, 2021

Choose a reason for hiding this comment

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

it seems cause downgrade error:

https://github.com/rectorphp/rector-src/runs/4505677100?check_suite_focus=true#step:14:80

-            $lastKey = array_key_last((array) $toStringClassMethod->stmts);
+            end((array) $toStringClassMethod->stmts);
+            $lastKey = key((array) $toStringClassMethod->stmts);

DowngradeArrayKeyFirstLastRector seems needs update for casting condition.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I created PR #1475 for it. It next need a tweak in DowngradeArrayKeyFirstLastRector to adapt this condition.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks 👍

$lastKey = $lastKey === null
? 0
: (int) $lastKey + 1;

$toStringClassMethod->stmts[$lastKey] = new Return_(new String_(''));

return;
}

$this->traverseNodesWithCallable((array) $toStringClassMethod->stmts, function (Node $subNode): void {
if (! $subNode instanceof Return_) {
return;
}

if (! $subNode->expr instanceof Expr) {
$subNode->expr = new String_('');
return;
}

$type = $this->nodeTypeResolver->getType($subNode->expr);
if ($type instanceof StringType) {
return;
}

$subNode->expr = new CastString_($subNode->expr);
});
}
}
45 changes: 45 additions & 0 deletions tests/Issues/StringableTypedProperty/Fixture/fixture.php.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace Rector\Core\Tests\Issues\StringableTypedProperty\Fixture;

class Fixture
{
private $nickname;

public function __toString()
{
return $this->nickname;
}

public function getNickname(): ?string
{
return $this->nickname;
}
}

?>
-----
<?php

declare(strict_types=1);

namespace Rector\Core\Tests\Issues\StringableTypedProperty\Fixture;

class Fixture implements \Stringable
{
private ?string $nickname = null;

public function __toString(): string
{
return (string) $this->nickname;
}

public function getNickname(): ?string
{
return $this->nickname;
}
}

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

declare(strict_types=1);

namespace Rector\Core\Tests\Issues\StringableTypedProperty;

use Iterator;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
use Symplify\SmartFileSystem\SmartFileInfo;

final class StringableTypedPropertyTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideData()
*/
public function test(SmartFileInfo $fileInfo): void
{
$this->doTestFileInfo($fileInfo);
}

/**
* @return Iterator<SmartFileInfo>
*/
public function provideData(): Iterator
{
return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
}

public function provideConfigFilePath(): string
{
return __DIR__ . '/config/configured_rule.php';
}
}
13 changes: 13 additions & 0 deletions tests/Issues/StringableTypedProperty/config/configured_rule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

use Rector\Php74\Rector\Property\TypedPropertyRector;
use Rector\Php80\Rector\Class_\StringableForToStringRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(StringableForToStringRector::class);
$services->set(TypedPropertyRector::class);
};