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

Adds the TypeHintTappableCallRector #244

Merged
merged 4 commits into from
Aug 31, 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
20 changes: 20 additions & 0 deletions docs/rector_rules_overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -1326,6 +1326,26 @@ Change if throw to throw_if

<br>

## TypeHintTappableCallRector

Automatically type hints your tappable closures

- class: [`RectorLaravel\Rector\FuncCall\TypeHintTappableCallRector`](../src/Rector/FuncCall/TypeHintTappableCallRector.php)

```diff
-tap($collection, function ($collection) {}
+tap($collection, function (Collection $collection) {}
```

<br>

```diff
-(new Collection)->tap(function ($collection) {}
+(new Collection)->tap(function (Collection $collection) {}
```

<br>

## UnifyModelDatesWithCastsRector

Unify Model `$dates` property with `$casts`
Expand Down
134 changes: 134 additions & 0 deletions src/Rector/FuncCall/TypeHintTappableCallRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<?php

namespace RectorLaravel\Rector\FuncCall;

use PhpParser\Node;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Param;
use PHPStan\Type\ObjectType;
use Rector\NodeTypeResolver\TypeComparator\TypeComparator;
use Rector\PHPStanStaticTypeMapper\Enum\TypeKind;
use Rector\Rector\AbstractRector;
use Rector\StaticTypeMapper\StaticTypeMapper;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @see \RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\TypeHintTappableCallRectorTest
*/
class TypeHintTappableCallRector extends AbstractRector
{
private const TAPPABLE_TRAIT = 'Illuminate\Support\Traits\Tappable';

public function __construct(
private readonly TypeComparator $typeComparator,
private readonly StaticTypeMapper $staticTypeMapper
) {
}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Automatically type hints your tappable closures',
[
new CodeSample(<<<'CODE_SAMPLE'
tap($collection, function ($collection) {}
CODE_SAMPLE,
<<<'CODE_SAMPLE'
tap($collection, function (Collection $collection) {}
CODE_SAMPLE
),
new CodeSample(<<<'CODE_SAMPLE'
(new Collection)->tap(function ($collection) {}
CODE_SAMPLE,
<<<'CODE_SAMPLE'
(new Collection)->tap(function (Collection $collection) {}
CODE_SAMPLE
),
]
);
}

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

/**
* @param MethodCall|FuncCall $node
*/
public function refactor(Node $node): ?Node
{
if (! $this->isName($node->name, 'tap')) {
return null;
}

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

if ($node instanceof MethodCall && $node->getArgs() !== []) {
return $this->refactorMethodCall($node);
}

if (count($node->getArgs()) < 2 || ! $node->getArgs()[1]->value instanceof Closure) {
return null;
}

/** @var Closure $closure */
$closure = $node->getArgs()[1]->value;

if ($closure->getParams() === []) {
return null;
}

$this->refactorParameter($closure->getParams()[0], $node->getArgs()[0]->value);
peterfox marked this conversation as resolved.
Show resolved Hide resolved

return $node;
}

private function refactorParameter(Param $param, Node $node): void
{
$nodePhpStanType = $this->nodeTypeResolver->getType($node);

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

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

private function refactorMethodCall(MethodCall $methodCall): ?MethodCall
{
if (! $this->isTappableCall($methodCall)) {
return null;
}

if (! $methodCall->getArgs()[0]->value instanceof Closure) {
return null;
}

/** @var Closure $closure */
$closure = $methodCall->getArgs()[0]->value;

if ($closure->getParams() === []) {
return null;
}

$this->refactorParameter($closure->getParams()[0], $methodCall->var);
GeniJaho marked this conversation as resolved.
Show resolved Hide resolved

return $methodCall;
}

private function isTappableCall(MethodCall $methodCall): bool
{
return $this->isObjectType($methodCall->var, new ObjectType(self::TAPPABLE_TRAIT));
}
}
14 changes: 14 additions & 0 deletions stubs/Illuminate/Support/Traits/Tappable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Illuminate\Support\Traits;

if (trait_exists('Illuminate\Support\Traits\Tappable')) {
return;
}

trait Tappable
{
public function tap($callback = null)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Fixture;

use RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Source\TappableExample;

$example = new TappableExample();

$example->tap(function ($example) {

});

?>
-----
<?php

namespace RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Fixture;

use RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Source\TappableExample;

$example = new TappableExample();

$example->tap(function (\RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Source\TappableExample $example) {

});

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

namespace RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Fixture;

use RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Source\TappableExample;

tap('test');

$example = new TappableExample();

$example->tap();

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

namespace RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Fixture;

use RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Source\TappableExample;

tap('test', function () {});

$example = new TappableExample();

$example->tap(function () {

});

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

namespace RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Fixture;

nottap('test', function ($string) {});

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

namespace RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Fixture;

use RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Source\NonTappableExample;

$example = new NonTappableExample();

$example->tap(function ($example) {

});

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

namespace RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Source;

class NonTappableExample
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Source;

use Illuminate\Support\Traits\Tappable;

class TappableExample
{
use Tappable;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector;

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

final class TypeHintTappableCallRectorTest extends AbstractRectorTestCase
{
public static function provideData(): Iterator
{
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
}

/**
* @test
*/
#[DataProvider('provideData')]
public function test(string $filePath): void
{
$this->doTestFile($filePath);
}

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

declare(strict_types=1);

use Rector\Config\RectorConfig;
use RectorLaravel\Rector\FuncCall\TypeHintTappableCallRector;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->import(__DIR__ . '/../../../../../config/config.php');

$rectorConfig->rule(TypeHintTappableCallRector::class);
};
Loading