From 5cf25b7698dbcd4f3156177562c009ec43bfbb6a Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Tue, 27 Aug 2024 23:48:05 +0100 Subject: [PATCH 1/3] Adds the TypeHintTappableCallRector --- docs/rector_rules_overview.md | 22 ++- .../FuncCall/TypeHintTappableCallRector.php | 128 ++++++++++++++++++ stubs/Illuminate/Support/Traits/Tappable.php | 14 ++ .../Fixture/fixture.php.inc | 4 +- ...p_where_already_using_class_string.php.inc | 2 +- .../Fixture/fixture.php.inc | 27 ++++ .../Fixture/skip_non_tap_calls.php.inc | 7 + .../Fixture/skip_non_tappable_object.php.inc | 13 ++ .../Source/NonTappableExample.php | 7 + .../Source/TappableExample.php | 8 ++ .../TypeHintTappableCallRectorTest.php | 31 +++++ .../config/configured_rule.php | 12 ++ 12 files changed, 271 insertions(+), 4 deletions(-) create mode 100644 src/Rector/FuncCall/TypeHintTappableCallRector.php create mode 100644 stubs/Illuminate/Support/Traits/Tappable.php create mode 100644 tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/fixture.php.inc create mode 100644 tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_non_tap_calls.php.inc create mode 100644 tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_non_tappable_object.php.inc create mode 100644 tests/Rector/FuncCall/TypeHintTappableCallRector/Source/NonTappableExample.php create mode 100644 tests/Rector/FuncCall/TypeHintTappableCallRector/Source/TappableExample.php create mode 100644 tests/Rector/FuncCall/TypeHintTappableCallRector/TypeHintTappableCallRectorTest.php create mode 100644 tests/Rector/FuncCall/TypeHintTappableCallRector/config/configured_rule.php diff --git a/docs/rector_rules_overview.md b/docs/rector_rules_overview.md index 16b4f746..9bb28e4d 100644 --- a/docs/rector_rules_overview.md +++ b/docs/rector_rules_overview.md @@ -1,4 +1,4 @@ -# 65 Rules Overview +# 66 Rules Overview ## AbortIfRector @@ -1306,6 +1306,26 @@ Change if throw to throw_if
+## 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) {} +``` + +
+ +```diff +-(new Collection)->tap(function ($collection) {} ++(new Collection)->tap(function (Collection $collection) {} +``` + +
+ ## UnifyModelDatesWithCastsRector Unify Model `$dates` property with `$casts` diff --git a/src/Rector/FuncCall/TypeHintTappableCallRector.php b/src/Rector/FuncCall/TypeHintTappableCallRector.php new file mode 100644 index 00000000..191d4162 --- /dev/null +++ b/src/Rector/FuncCall/TypeHintTappableCallRector.php @@ -0,0 +1,128 @@ +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; + + $this->refactorParameter($closure->getParams()[0], $node->getArgs()[0]->value); + + return $node; + } + + private function refactorParameter(Param $param, Node $node): bool + { + $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 false; + } + } + + $paramTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($nodePhpStanType, TypeKind::PARAM); + $param->type = $paramTypeNode; + + return true; + } + + 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; + + $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)); + } +} diff --git a/stubs/Illuminate/Support/Traits/Tappable.php b/stubs/Illuminate/Support/Traits/Tappable.php new file mode 100644 index 00000000..e70c2934 --- /dev/null +++ b/stubs/Illuminate/Support/Traits/Tappable.php @@ -0,0 +1,14 @@ +tap(function ($example) { + +}); + +?> +----- +tap(function (\RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Source\TappableExample $example) { + +}); + +?> diff --git a/tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_non_tap_calls.php.inc b/tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_non_tap_calls.php.inc new file mode 100644 index 00000000..00df2fb6 --- /dev/null +++ b/tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_non_tap_calls.php.inc @@ -0,0 +1,7 @@ + diff --git a/tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_non_tappable_object.php.inc b/tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_non_tappable_object.php.inc new file mode 100644 index 00000000..58b21b9f --- /dev/null +++ b/tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_non_tappable_object.php.inc @@ -0,0 +1,13 @@ +tap(function ($example) { + +}); + +?> diff --git a/tests/Rector/FuncCall/TypeHintTappableCallRector/Source/NonTappableExample.php b/tests/Rector/FuncCall/TypeHintTappableCallRector/Source/NonTappableExample.php new file mode 100644 index 00000000..94860e79 --- /dev/null +++ b/tests/Rector/FuncCall/TypeHintTappableCallRector/Source/NonTappableExample.php @@ -0,0 +1,7 @@ +doTestFile($filePath); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/tests/Rector/FuncCall/TypeHintTappableCallRector/config/configured_rule.php b/tests/Rector/FuncCall/TypeHintTappableCallRector/config/configured_rule.php new file mode 100644 index 00000000..99084998 --- /dev/null +++ b/tests/Rector/FuncCall/TypeHintTappableCallRector/config/configured_rule.php @@ -0,0 +1,12 @@ +import(__DIR__ . '/../../../../../config/config.php'); + + $rectorConfig->rule(TypeHintTappableCallRector::class); +}; From f82704cbf83167b66207515725e42f010f31e1f2 Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Thu, 29 Aug 2024 22:28:38 +0100 Subject: [PATCH 2/3] fix import --- .../TypeHintTappableCallRector/Source/TappableExample.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Rector/FuncCall/TypeHintTappableCallRector/Source/TappableExample.php b/tests/Rector/FuncCall/TypeHintTappableCallRector/Source/TappableExample.php index 874c6121..c73413b1 100644 --- a/tests/Rector/FuncCall/TypeHintTappableCallRector/Source/TappableExample.php +++ b/tests/Rector/FuncCall/TypeHintTappableCallRector/Source/TappableExample.php @@ -2,7 +2,9 @@ namespace RectorLaravel\Tests\Rector\FuncCall\TypeHintTappableCallRector\Source; +use Illuminate\Support\Traits\Tappable; + class TappableExample { - use \Illuminate\Support\Traits\Tappable; + use Tappable; } From 8a4878e50faf009c6076ec6ab7f62ff5455c24a8 Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Sat, 31 Aug 2024 14:17:52 +0100 Subject: [PATCH 3/3] Feedback fixes --- .../FuncCall/TypeHintTappableCallRector.php | 14 ++++++++++---- .../Fixture/fixture.php.inc | 4 ++-- .../skip_where_already_using_class_string.php.inc | 2 +- .../Fixture/skip_missing_args.php.inc | 13 +++++++++++++ .../skip_missing_closure_parameter.php.inc | 15 +++++++++++++++ 5 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_missing_args.php.inc create mode 100644 tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_missing_closure_parameter.php.inc diff --git a/src/Rector/FuncCall/TypeHintTappableCallRector.php b/src/Rector/FuncCall/TypeHintTappableCallRector.php index 191d4162..3c7a2e92 100644 --- a/src/Rector/FuncCall/TypeHintTappableCallRector.php +++ b/src/Rector/FuncCall/TypeHintTappableCallRector.php @@ -80,12 +80,16 @@ public function refactor(Node $node): ?Node /** @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): bool + private function refactorParameter(Param $param, Node $node): void { $nodePhpStanType = $this->nodeTypeResolver->getType($node); @@ -93,14 +97,12 @@ private function refactorParameter(Param $param, Node $node): bool if ($param->type instanceof Node) { $currentParamType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($param->type); if ($this->typeComparator->areTypesEqual($currentParamType, $nodePhpStanType)) { - return false; + return; } } $paramTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($nodePhpStanType, TypeKind::PARAM); $param->type = $paramTypeNode; - - return true; } private function refactorMethodCall(MethodCall $methodCall): ?MethodCall @@ -116,6 +118,10 @@ private function refactorMethodCall(MethodCall $methodCall): ?MethodCall /** @var Closure $closure */ $closure = $methodCall->getArgs()[0]->value; + if ($closure->getParams() === []) { + return null; + } + $this->refactorParameter($closure->getParams()[0], $methodCall->var); return $methodCall; diff --git a/tests/Rector/FuncCall/ThrowIfAndThrowUnlessExceptionsToUseClassStringRector/Fixture/fixture.php.inc b/tests/Rector/FuncCall/ThrowIfAndThrowUnlessExceptionsToUseClassStringRector/Fixture/fixture.php.inc index 03c2eb3b..0b9b3f96 100644 --- a/tests/Rector/FuncCall/ThrowIfAndThrowUnlessExceptionsToUseClassStringRector/Fixture/fixture.php.inc +++ b/tests/Rector/FuncCall/ThrowIfAndThrowUnlessExceptionsToUseClassStringRector/Fixture/fixture.php.inc @@ -1,6 +1,6 @@ tap(); + +?> diff --git a/tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_missing_closure_parameter.php.inc b/tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_missing_closure_parameter.php.inc new file mode 100644 index 00000000..15c180c6 --- /dev/null +++ b/tests/Rector/FuncCall/TypeHintTappableCallRector/Fixture/skip_missing_closure_parameter.php.inc @@ -0,0 +1,15 @@ +tap(function () { + +}); + +?>