Skip to content

Commit

Permalink
Adds the TypeHintTappableCallRector (#244)
Browse files Browse the repository at this point in the history
* Adds the TypeHintTappableCallRector

* fix import

* Feedback fixes
  • Loading branch information
peterfox authored Aug 31, 2024
1 parent 037f997 commit ca45e6d
Show file tree
Hide file tree
Showing 12 changed files with 303 additions and 0 deletions.
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);

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);

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);
};

0 comments on commit ca45e6d

Please sign in to comment.