diff --git a/config/sets/laravel-code-quality.php b/config/sets/laravel-code-quality.php index e8ba5935..66d12109 100644 --- a/config/sets/laravel-code-quality.php +++ b/config/sets/laravel-code-quality.php @@ -4,10 +4,12 @@ use Rector\Config\RectorConfig; use RectorLaravel\Rector\Assign\CallOnAppArrayAccessToStandaloneAssignRector; +use RectorLaravel\Rector\Coalesce\ApplyDefaultInsteadOfNullCoalesceRector; use RectorLaravel\Rector\MethodCall\ReverseConditionableMethodCallRector; return static function (RectorConfig $rectorConfig): void { $rectorConfig->import(__DIR__ . '/../config.php'); $rectorConfig->rule(CallOnAppArrayAccessToStandaloneAssignRector::class); $rectorConfig->rule(ReverseConditionableMethodCallRector::class); + $rectorConfig->rule(ApplyDefaultInsteadOfNullCoalesceRector::class); }; diff --git a/docs/rector_rules_overview.md b/docs/rector_rules_overview.md index 906f4dea..3be2e93d 100644 --- a/docs/rector_rules_overview.md +++ b/docs/rector_rules_overview.md @@ -1,4 +1,4 @@ -# 71 Rules Overview +# 72 Rules Overview ## AbortIfRector @@ -221,6 +221,21 @@ Replace `$app->environment() === 'local'` with `$app->environment('local')`
+## ApplyDefaultInsteadOfNullCoalesceRector + +Apply default instead of null coalesce + +:wrench: **configure it!** + +- class: [`RectorLaravel\Rector\Coalesce\ApplyDefaultInsteadOfNullCoalesceRector`](../src/Rector/Coalesce/ApplyDefaultInsteadOfNullCoalesceRector.php) + +```diff +-custom_helper('app.name') ?? 'Laravel'; ++custom_helper('app.name', 'Laravel'); +``` + +
+ ## ArgumentFuncCallToMethodCallRector Move help facade-like function calls to constructor injection diff --git a/src/Rector/Coalesce/ApplyDefaultInsteadOfNullCoalesceRector.php b/src/Rector/Coalesce/ApplyDefaultInsteadOfNullCoalesceRector.php new file mode 100644 index 00000000..6167dc31 --- /dev/null +++ b/src/Rector/Coalesce/ApplyDefaultInsteadOfNullCoalesceRector.php @@ -0,0 +1,136 @@ +applyDefaultWith = self::defaultLaravelMethods(); + } + + /** + * @return ApplyDefaultInsteadOfNullCoalesce[] + */ + public static function defaultLaravelMethods(): array + { + return [ + new ApplyDefaultInsteadOfNullCoalesce('config'), + new ApplyDefaultInsteadOfNullCoalesce('env'), + new ApplyDefaultInsteadOfNullCoalesce('data_get', argumentPosition: 2), + new ApplyDefaultInsteadOfNullCoalesce('input', new ObjectType('Illuminate\Http\Request')), + new ApplyDefaultInsteadOfNullCoalesce('get', new ObjectType('Illuminate\Support\Env')), + ]; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Apply default instead of null coalesce', + [ + new ConfiguredCodeSample( + <<<'CODE_SAMPLE' +custom_helper('app.name') ?? 'Laravel'; +CODE_SAMPLE, + <<<'CODE_SAMPLE' +custom_helper('app.name', 'Laravel'); +CODE_SAMPLE + , + [ + ...self::defaultLaravelMethods(), + new ApplyDefaultInsteadOfNullCoalesce('custom_helper'), + ], + ), + ] + ); + } + + public function getNodeTypes(): array + { + return [Coalesce::class]; + } + + /** + * @param Coalesce $node + */ + public function refactor(Node $node): MethodCall|StaticCall|FuncCall|null + { + if (! $node->left instanceof FuncCall && + ! $node->left instanceof MethodCall && + ! $node->left instanceof StaticCall + ) { + return null; + } + + if ($node->left->isFirstClassCallable()) { + return null; + } + + $call = $node->left; + + foreach ($this->applyDefaultWith as $applyDefaultWith) { + $valid = false; + + $objectType = $call->var ?? $call->class ?? null; + + if ( + $applyDefaultWith->getObjectType() instanceof ObjectType && + $objectType !== null && + $this->isObjectType( + $objectType, + $applyDefaultWith->getObjectType()) && + $this->isName($call->name, $applyDefaultWith->getMethodName()) + ) { + $valid = true; + } elseif ( + $applyDefaultWith->getObjectType() === null && + $this->isName($call->name, $applyDefaultWith->getMethodName()) + ) { + $valid = true; + } + + if (! $valid) { + continue; + } + + if (count($call->args) === $applyDefaultWith->getArgumentPosition()) { + if ($this->getType($node->right)->isNull()->yes()) { + return $call; + } + $call->args[count($call->args)] = new Arg($node->right); + + return $call; + } + } + + return null; + } + + public function configure(array $configuration): void + { + Assert::allIsInstanceOf($configuration, ApplyDefaultInsteadOfNullCoalesce::class); + $this->applyDefaultWith = $configuration; + } +} diff --git a/src/ValueObject/ApplyDefaultInsteadOfNullCoalesce.php b/src/ValueObject/ApplyDefaultInsteadOfNullCoalesce.php new file mode 100644 index 00000000..20766a80 --- /dev/null +++ b/src/ValueObject/ApplyDefaultInsteadOfNullCoalesce.php @@ -0,0 +1,27 @@ +objectType; + } + + public function getMethodName(): string + { + return $this->methodName; + } + + public function getArgumentPosition(): int + { + return $this->argumentPosition; + } +} diff --git a/tests/Rector/Coalesce/ApplyDefaultInsteadOfNullCoalesceRector/ApplyDefaultInsteadOfNullCoalesceRectorTest.php b/tests/Rector/Coalesce/ApplyDefaultInsteadOfNullCoalesceRector/ApplyDefaultInsteadOfNullCoalesceRectorTest.php new file mode 100644 index 00000000..54726842 --- /dev/null +++ b/tests/Rector/Coalesce/ApplyDefaultInsteadOfNullCoalesceRector/ApplyDefaultInsteadOfNullCoalesceRectorTest.php @@ -0,0 +1,31 @@ +doTestFile($filePath); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/tests/Rector/Coalesce/ApplyDefaultInsteadOfNullCoalesceRector/Fixture/applies_based_on_argument_position.php.inc b/tests/Rector/Coalesce/ApplyDefaultInsteadOfNullCoalesceRector/Fixture/applies_based_on_argument_position.php.inc new file mode 100644 index 00000000..8a66bfd3 --- /dev/null +++ b/tests/Rector/Coalesce/ApplyDefaultInsteadOfNullCoalesceRector/Fixture/applies_based_on_argument_position.php.inc @@ -0,0 +1,15 @@ + +----- + diff --git a/tests/Rector/Coalesce/ApplyDefaultInsteadOfNullCoalesceRector/Fixture/fixture.php.inc b/tests/Rector/Coalesce/ApplyDefaultInsteadOfNullCoalesceRector/Fixture/fixture.php.inc new file mode 100644 index 00000000..50560f87 --- /dev/null +++ b/tests/Rector/Coalesce/ApplyDefaultInsteadOfNullCoalesceRector/Fixture/fixture.php.inc @@ -0,0 +1,25 @@ +input('value') ?? ''; + +\Illuminate\Support\Env::get('APP_NAME') ?? ''; + +?> +----- +input('value', ''); + +\Illuminate\Support\Env::get('APP_NAME', ''); + +?> diff --git a/tests/Rector/Coalesce/ApplyDefaultInsteadOfNullCoalesceRector/Fixture/skip_already_set_default.php.inc b/tests/Rector/Coalesce/ApplyDefaultInsteadOfNullCoalesceRector/Fixture/skip_already_set_default.php.inc new file mode 100644 index 00000000..81c39e37 --- /dev/null +++ b/tests/Rector/Coalesce/ApplyDefaultInsteadOfNullCoalesceRector/Fixture/skip_already_set_default.php.inc @@ -0,0 +1,11 @@ +input('value', '') ?? ''; + +\Illuminate\Support\Env::get('APP_NAME', '') ?? ''; + +?> diff --git a/tests/Rector/Coalesce/ApplyDefaultInsteadOfNullCoalesceRector/Fixture/skip_non_matching_cases.php.inc b/tests/Rector/Coalesce/ApplyDefaultInsteadOfNullCoalesceRector/Fixture/skip_non_matching_cases.php.inc new file mode 100644 index 00000000..3feb94cc --- /dev/null +++ b/tests/Rector/Coalesce/ApplyDefaultInsteadOfNullCoalesceRector/Fixture/skip_non_matching_cases.php.inc @@ -0,0 +1,13 @@ +inputOf('value') ?? ''; +(new \Illuminate\Http\RequestElse())->input('value') ?? ''; + +\Illuminate\Support\Env::getAgain('APP_NAME') ?? ''; +\Illuminate\Support\EnvUtil::get('APP_NAME') ?? ''; + +?> diff --git a/tests/Rector/Coalesce/ApplyDefaultInsteadOfNullCoalesceRector/config/configured_rule.php b/tests/Rector/Coalesce/ApplyDefaultInsteadOfNullCoalesceRector/config/configured_rule.php new file mode 100644 index 00000000..755e2576 --- /dev/null +++ b/tests/Rector/Coalesce/ApplyDefaultInsteadOfNullCoalesceRector/config/configured_rule.php @@ -0,0 +1,12 @@ +import(__DIR__ . '/../../../../../config/config.php'); + + $rectorConfig->rule(ApplyDefaultInsteadOfNullCoalesceRector::class); +};