diff --git a/config/sets/laravel-collection.php b/config/sets/laravel-collection.php index 84cd852d..c7618c90 100644 --- a/config/sets/laravel-collection.php +++ b/config/sets/laravel-collection.php @@ -3,9 +3,11 @@ declare(strict_types=1); use Rector\Config\RectorConfig; +use RectorLaravel\Rector\BooleanNot\AvoidNegatedCollectionContainsOrDoesntContainRector; use RectorLaravel\Rector\MethodCall\AvoidNegatedCollectionFilterOrRejectRector; return static function (RectorConfig $rectorConfig): void { $rectorConfig->import(__DIR__ . '/../config.php'); + $rectorConfig->rule(AvoidNegatedCollectionContainsOrDoesntContainRector::class); $rectorConfig->rule(AvoidNegatedCollectionFilterOrRejectRector::class); }; diff --git a/src/Rector/BooleanNot/AvoidNegatedCollectionContainsOrDoesntContainRector.php b/src/Rector/BooleanNot/AvoidNegatedCollectionContainsOrDoesntContainRector.php new file mode 100644 index 00000000..9b03e145 --- /dev/null +++ b/src/Rector/BooleanNot/AvoidNegatedCollectionContainsOrDoesntContainRector.php @@ -0,0 +1,87 @@ +contains(fn (?int $number): bool => is_null($number)); +! $collection->doesntContain(fn (?int $number) => $number > 0); +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +use Illuminate\Support\Collection; + +$collection = new Collection([0, 1, null, -1]); +$collection->doesntContain(fn (?int $number): bool => is_null($number)); +$collection->contains(fn (?int $number) => $number > 0); +CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [BooleanNot::class]; + } + + /** + * @param BooleanNot $node + */ + public function refactor(Node $node): ?Node + { + return $this->updateBooleanNot($node); + } + + private function updateBooleanNot(BooleanNot $booleanNot): ?MethodCall + { + $expr = $booleanNot->expr; + if (! $expr instanceof MethodCall) { + return null; + } + + if (! $this->isObjectType($expr->var, new ObjectType('Illuminate\Support\Enumerable'))) { + return null; + } + + $name = $expr->name; + if ($this->isName($name, 'contains')) { + $replacement = 'doesntContain'; + } elseif ($this->isName($name, 'doesntContain')) { + $replacement = 'contains'; + } else { + return null; + } + + $expr->name = new Identifier($replacement); + + return $expr; + } +} diff --git a/stubs/Illuminate/Support/Enumerable.php b/stubs/Illuminate/Support/Enumerable.php index cb7f6a84..505f77e6 100644 --- a/stubs/Illuminate/Support/Enumerable.php +++ b/stubs/Illuminate/Support/Enumerable.php @@ -18,6 +18,26 @@ */ interface Enumerable { + /** + * Determine if an item exists in the enumerable. + * + * @param (callable(TValue, TKey): bool)|TValue|string $key + * @param mixed $operator + * @param mixed $value + * @return bool + */ + public function contains($key, $operator = null, $value = null); + + /** + * Determine if an item is not contained in the collection. + * + * @param mixed $key + * @param mixed $operator + * @param mixed $value + * @return bool + */ + public function doesntContain($key, $operator = null, $value = null); + /** * Run a filter over each of the items. * diff --git a/tests/Rector/BooleanNot/AvoidNegatedCollectionContainsOrDoesntContainRector/AvoidNegatedCollectionContainsOrDoesntContainRectorTest.php b/tests/Rector/BooleanNot/AvoidNegatedCollectionContainsOrDoesntContainRector/AvoidNegatedCollectionContainsOrDoesntContainRectorTest.php new file mode 100644 index 00000000..4d7c9068 --- /dev/null +++ b/tests/Rector/BooleanNot/AvoidNegatedCollectionContainsOrDoesntContainRector/AvoidNegatedCollectionContainsOrDoesntContainRectorTest.php @@ -0,0 +1,31 @@ +doTestFile($filePath); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/tests/Rector/BooleanNot/AvoidNegatedCollectionContainsOrDoesntContainRector/Fixture/fixture_contains.php.inc b/tests/Rector/BooleanNot/AvoidNegatedCollectionContainsOrDoesntContainRector/Fixture/fixture_contains.php.inc new file mode 100644 index 00000000..ae9de2b2 --- /dev/null +++ b/tests/Rector/BooleanNot/AvoidNegatedCollectionContainsOrDoesntContainRector/Fixture/fixture_contains.php.inc @@ -0,0 +1,31 @@ +contains(fn (?int $number): bool => is_null($number)); +! $collection->contains(fn (?int $number): bool => is_null($number)); +! $collection->contains(function (?int $number): bool { + return is_null($number); +}); +! $collection->contains(1); + +?> +----- +contains(fn (?int $number): bool => is_null($number)); +$collection->doesntContain(fn (?int $number): bool => is_null($number)); +$collection->doesntContain(function (?int $number): bool { + return is_null($number); +}); +$collection->doesntContain(1); + +?> diff --git a/tests/Rector/BooleanNot/AvoidNegatedCollectionContainsOrDoesntContainRector/Fixture/fixture_doesntContain.php.inc b/tests/Rector/BooleanNot/AvoidNegatedCollectionContainsOrDoesntContainRector/Fixture/fixture_doesntContain.php.inc new file mode 100644 index 00000000..5cd3e016 --- /dev/null +++ b/tests/Rector/BooleanNot/AvoidNegatedCollectionContainsOrDoesntContainRector/Fixture/fixture_doesntContain.php.inc @@ -0,0 +1,31 @@ +doesntContain(fn (?int $number): bool => is_null($number)); +! $collection->doesntContain(fn (?int $number): bool => is_null($number)); +! $collection->doesntContain(function (?int $number): bool { + return is_null($number); +}); +! $collection->doesntContain(1); + +?> +----- +doesntContain(fn (?int $number): bool => is_null($number)); +$collection->contains(fn (?int $number): bool => is_null($number)); +$collection->contains(function (?int $number): bool { + return is_null($number); +}); +$collection->contains(1); + +?> diff --git a/tests/Rector/BooleanNot/AvoidNegatedCollectionContainsOrDoesntContainRector/config/configured_rule.php b/tests/Rector/BooleanNot/AvoidNegatedCollectionContainsOrDoesntContainRector/config/configured_rule.php new file mode 100644 index 00000000..389294b5 --- /dev/null +++ b/tests/Rector/BooleanNot/AvoidNegatedCollectionContainsOrDoesntContainRector/config/configured_rule.php @@ -0,0 +1,11 @@ +import(__DIR__ . '/../../../../../config/config.php'); + $rectorConfig->rule(AvoidNegatedCollectionContainsOrDoesntContainRector::class); +};