From b898cffcf043dd6d25e6f9220c7f1de2ac3ddfb1 Mon Sep 17 00:00:00 2001 From: Caleb White Date: Sat, 26 Jul 2025 20:32:54 -0500 Subject: [PATCH] feat: add support for set, isset, and unset operations in ArrayDimFetchToMethodCallRector --- .../Fixture/fixture.php.inc | 10 +- .../Fixture/partial_tranformation.php.inc | 23 +++ .../transforms_multiple_issets.php.inc | 23 +++ .../transforms_multiple_unsets.php.inc | 21 +++ .../Fixture/transforms_property_fetch.php.inc | 8 +- .../config/configured_rule.php | 3 +- .../ArrayDimFetchToMethodCallRector.php | 151 ++++++++++++++++-- .../ValueObject/ArrayDimFetchToMethodCall.php | 22 ++- 8 files changed, 243 insertions(+), 18 deletions(-) create mode 100644 rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/Fixture/partial_tranformation.php.inc create mode 100644 rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/Fixture/transforms_multiple_issets.php.inc create mode 100644 rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/Fixture/transforms_multiple_unsets.php.inc diff --git a/rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/Fixture/fixture.php.inc b/rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/Fixture/fixture.php.inc index 99245704875..918de0ed276 100644 --- a/rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/Fixture/fixture.php.inc +++ b/rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/Fixture/fixture.php.inc @@ -3,7 +3,10 @@ namespace Rector\Tests\Transform\Rector\ArrayDimFetch\ArrayDimFetchToMethodCallRector\Fixture; /** @var \SomeClass $object */ -$object['key']->get() +$object['key']; +$object['key'] = 42; +isset($object['key']); +unset($object['key']); ?> ----- @@ -12,6 +15,9 @@ $object['key']->get() namespace Rector\Tests\Transform\Rector\ArrayDimFetch\ArrayDimFetchToMethodCallRector\Fixture; /** @var \SomeClass $object */ -$object->make('key')->get() +$object->get('key'); +$object->set('key', 42); +$object->has('key'); +$object->unset('key'); ?> diff --git a/rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/Fixture/partial_tranformation.php.inc b/rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/Fixture/partial_tranformation.php.inc new file mode 100644 index 00000000000..a2963148164 --- /dev/null +++ b/rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/Fixture/partial_tranformation.php.inc @@ -0,0 +1,23 @@ + +----- +get('key'); +$object['key'] = 'value'; +isset($object['key']); +unset($object['key']); + +?> diff --git a/rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/Fixture/transforms_multiple_issets.php.inc b/rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/Fixture/transforms_multiple_issets.php.inc new file mode 100644 index 00000000000..70525585214 --- /dev/null +++ b/rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/Fixture/transforms_multiple_issets.php.inc @@ -0,0 +1,23 @@ + +----- +has('key1') && $object->has('key2')) { + echo 'Keys are set'; +} + +?> diff --git a/rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/Fixture/transforms_multiple_unsets.php.inc b/rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/Fixture/transforms_multiple_unsets.php.inc new file mode 100644 index 00000000000..6974b05f432 --- /dev/null +++ b/rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/Fixture/transforms_multiple_unsets.php.inc @@ -0,0 +1,21 @@ + +----- +unset('key1'); +$object->unset('key2'); + +?> diff --git a/rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/Fixture/transforms_property_fetch.php.inc b/rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/Fixture/transforms_property_fetch.php.inc index a1a64a18675..fb7d9fd2671 100644 --- a/rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/Fixture/transforms_property_fetch.php.inc +++ b/rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/Fixture/transforms_property_fetch.php.inc @@ -9,6 +9,9 @@ class FooBar extends SomeExtendedClass public function someMethod() { $this->something['key']; + $this->something['key'] = 42; + isset($this->something['key']); + unset($this->something['key']); } } @@ -24,7 +27,10 @@ class FooBar extends SomeExtendedClass { public function someMethod() { - $this->something->make('key'); + $this->something->get('key'); + $this->something->set('key', 42); + $this->something->has('key'); + $this->something->unset('key'); } } diff --git a/rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/config/configured_rule.php b/rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/config/configured_rule.php index 28cf90e2cda..9eb2f988d02 100644 --- a/rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/config/configured_rule.php +++ b/rules-tests/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector/config/configured_rule.php @@ -10,6 +10,7 @@ return static function (RectorConfig $rectorConfig): void { $rectorConfig ->ruleWithConfiguration(ArrayDimFetchToMethodCallRector::class, [ - new ArrayDimFetchToMethodCall(new ObjectType('SomeClass'), 'make'), + new ArrayDimFetchToMethodCall(new ObjectType('SomeClass'), 'get', 'set', 'has', 'unset'), + new ArrayDimFetchToMethodCall(new ObjectType('Container'), 'get'), ]); }; diff --git a/rules/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector.php b/rules/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector.php index 663f02dddd5..ddbb4528ada 100644 --- a/rules/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector.php +++ b/rules/Transform/Rector/ArrayDimFetch/ArrayDimFetchToMethodCallRector.php @@ -5,10 +5,18 @@ namespace Rector\Transform\Rector\ArrayDimFetch; use PhpParser\Node; +use PhpParser\NodeVisitor; use PhpParser\Node\Arg; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\ArrayDimFetch; +use PhpParser\Node\Expr\Assign; +use PhpParser\Node\Expr\BinaryOp\BooleanAnd; +use PhpParser\Node\Expr\Isset_; use PhpParser\Node\Expr\MethodCall; use PHPStan\Type\ObjectType; +use PhpParser\Node\Stmt; +use PhpParser\Node\Stmt\Expression; +use PhpParser\Node\Stmt\Unset_; use Rector\Contract\Rector\ConfigurableRectorInterface; use Rector\Rector\AbstractRector; use Rector\Transform\ValueObject\ArrayDimFetchToMethodCall; @@ -31,41 +39,54 @@ public function getRuleDefinition(): RuleDefinition return new RuleDefinition('Change array dim fetch to method call', [ new ConfiguredCodeSample( <<<'CODE_SAMPLE' -$app['someService']; +$object['key']; +$object['key'] = 'value'; +isset($object['key']); +unset($object['key']); CODE_SAMPLE , <<<'CODE_SAMPLE' -$app->make('someService'); +$object->get('key'); +$object->set('key', 'value'); +$object->has('key'); +$object->unset('key'); CODE_SAMPLE , - [new ArrayDimFetchToMethodCall(new ObjectType('SomeClass'), 'make')] + [new ArrayDimFetchToMethodCall(new ObjectType('SomeClass'), 'get', 'set', 'has', 'unset')], ), ]); } public function getNodeTypes(): array { - return [ArrayDimFetch::class]; + return [Assign::class, Isset_::class, Unset_::class, ArrayDimFetch::class]; } /** - * @param ArrayDimFetch $node + * @template TNode of ArrayDimFetch|Assign|Isset_|Unset_ + * @param TNode $node + * @return ($node is Unset_ ? Stmt[]|int : ($node is Isset_ ? Expr|int : MethodCall|int|null)) */ - public function refactor(Node $node): ?MethodCall + public function refactor(Node $node): array|Expr|null|int { - if (! $node->dim instanceof Node) { - return null; + if ($node instanceof Unset_) { + return $this->handleUnset($node); } - foreach ($this->arrayDimFetchToMethodCalls as $arrayDimFetchToMethodCall) { - if (! $this->isObjectType($node->var, $arrayDimFetchToMethodCall->getObjectType())) { - continue; + if ($node instanceof Isset_) { + return $this->handleIsset($node); + } + + if ($node instanceof Assign) { + if (!$node->var instanceof ArrayDimFetch) { + return null; } - return new MethodCall($node->var, $arrayDimFetchToMethodCall->getMethod(), [new Arg($node->dim)]); + return $this->getMethodCall($node->var, 'set', $node->expr) + ?? NodeVisitor::DONT_TRAVERSE_CHILDREN; } - return null; + return $this->getMethodCall($node, 'get'); } public function configure(array $configuration): void @@ -74,4 +95,108 @@ public function configure(array $configuration): void $this->arrayDimFetchToMethodCalls = $configuration; } + + private function handleIsset(Isset_ $node): Expr|int|null + { + $issets = []; + $exprs = []; + + foreach ($node->vars as $var) { + if ($var instanceof ArrayDimFetch) { + $methodCall = $this->getMethodCall($var, 'exists'); + + if ($methodCall !== null) { + $exprs[] = $methodCall; + continue; + } + } + + $issets[] = $var; + } + + if ($exprs === []) { + return NodeVisitor::DONT_TRAVERSE_CHILDREN; + } + + if ($issets !== []) { + $node->vars = $issets; + array_unshift($exprs, $node); + } + + return array_reduce( + $exprs, + fn (?Expr $carry, Expr $expr) => $carry === null ? $expr : new BooleanAnd($carry, $expr), + null, + ); + } + + /** + * @return Stmt[]|int + */ + private function handleUnset(Unset_ $node): array|int + { + $unsets = []; + $stmts = []; + + foreach ($node->vars as $var) { + if ($var instanceof ArrayDimFetch) { + $methodCall = $this->getMethodCall($var, 'unset'); + + if ($methodCall !== null) { + $stmts[] = new Expression($methodCall); + continue; + } + } + + $unsets[] = $var; + } + + if ($stmts === []) { + return NodeVisitor::DONT_TRAVERSE_CHILDREN; + } + + if ($unsets !== []) { + $node->vars = $unsets; + array_unshift($stmts, $node); + } + + return $stmts; + } + + /** + * @param 'get'|'set'|'exists'|'unset' $action + */ + private function getMethodCall(ArrayDimFetch $fetch, string $action, ?Expr $value = null): ?MethodCall + { + if (!$fetch->dim instanceof Node) { + return null; + } + + foreach ($this->arrayDimFetchToMethodCalls as $arrayDimFetchToMethodCall) { + if (!$this->isObjectType($fetch->var, $arrayDimFetchToMethodCall->getObjectType())) { + continue; + } + + $method = match ($action) { + 'get' => $arrayDimFetchToMethodCall->getMethod(), + 'set' => $arrayDimFetchToMethodCall->getSetMethod(), + 'exists' => $arrayDimFetchToMethodCall->getExistsMethod(), + 'unset' => $arrayDimFetchToMethodCall->getUnsetMethod(), + }; + + if ($method === null) { + continue; + } + + $args = [new Arg($fetch->dim)]; + + if ($value instanceof Expr) { + $args[] = new Arg($value); + } + + return new MethodCall($fetch->var, $method, $args); + } + + return null; + } } diff --git a/rules/Transform/ValueObject/ArrayDimFetchToMethodCall.php b/rules/Transform/ValueObject/ArrayDimFetchToMethodCall.php index 43b8a29b3e5..70600a83a38 100644 --- a/rules/Transform/ValueObject/ArrayDimFetchToMethodCall.php +++ b/rules/Transform/ValueObject/ArrayDimFetchToMethodCall.php @@ -10,7 +10,12 @@ class ArrayDimFetchToMethodCall { public function __construct( private readonly ObjectType $objectType, - private readonly string $method + private readonly string $method, + // Optional methods for set, exists, and unset operations + // if null, then these operations will not be transformed + private readonly ?string $setMethod = null, + private readonly ?string $existsMethod = null, + private readonly ?string $unsetMethod = null, ) { } @@ -23,4 +28,19 @@ public function getMethod(): string { return $this->method; } + + public function getSetMethod(): ?string + { + return $this->setMethod; + } + + public function getExistsMethod(): ?string + { + return $this->existsMethod; + } + + public function getUnsetMethod(): ?string + { + return $this->unsetMethod; + } }