diff --git a/resources/schema.json b/resources/schema.json index 43ffc4f63..5a37a82f4 100644 --- a/resources/schema.json +++ b/resources/schema.json @@ -115,6 +115,35 @@ } ] } + }, + "ArrayItemRemoval": { + "type": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "settings": { + "type": "object", + "additionalProperties": false, + "properties": { + "remove": { + "type": "string", + "enum": ["first", "last", "all"] + }, + "limit": { + "type": "integer", + "minimum": 1 + } + } + } + } + } + ] + } } } }, diff --git a/src/Mutator/Removal/ArrayItemRemoval.php b/src/Mutator/Removal/ArrayItemRemoval.php new file mode 100644 index 000000000..992dd9238 --- /dev/null +++ b/src/Mutator/Removal/ArrayItemRemoval.php @@ -0,0 +1,130 @@ + 'first', + 'limit' => PHP_INT_MAX, + ]; + + /** @var string first|last|all */ + private $remove; + /** @var int */ + private $limit; + + public function __construct(MutatorConfig $config) + { + parent::__construct($config); + + $settings = $this->getResultSettings(); + + $this->remove = $settings['remove']; + $this->limit = $settings['limit']; + } + + /** + * @param Node|Node\Expr\Array_ $arrayNode + */ + public function mutate(Node $arrayNode): Generator + { + foreach ($this->getItemsIndexes($arrayNode->items) as $indexToRemove) { + $newArrayNode = clone $arrayNode; + unset($newArrayNode->items[$indexToRemove]); + + yield $newArrayNode; + } + } + + protected function mutatesNode(Node $node): bool + { + return $node instanceof Node\Expr\Array_ && \count($node->items); + } + + private function getItemsIndexes(array $items): array + { + switch ($this->remove) { + case 'first': + return [0]; + case 'last': + return [\count($items) - 1]; + default: + return \range(0, \min(\count($items), $this->limit) - 1); + } + } + + private function getResultSettings(): array + { + $settings = \array_merge(self::DEFAULT_SETTINGS, $this->getSettings()); + + if (!\is_string($settings['remove'])) { + $this->throwConfigException('remove'); + } + + $settings['remove'] = \strtolower($settings['remove']); + + if (!\in_array($settings['remove'], ['first', 'last', 'all'])) { + $this->throwConfigException('remove'); + } + + if (!\is_numeric($settings['limit']) || $settings['limit'] < 1) { + $this->throwConfigException('limit'); + } + + return $settings; + } + + private function throwConfigException(string $property): void + { + $value = $this->getSettings()[$property]; + + throw new InvalidConfigException(sprintf( + 'Invalid configuration of ArrayItemRemoval mutator. Setting `%s` is invalid (%s)', + $property, + \is_scalar($value) ? $value : '<' . \strtoupper(\gettype($value)) . '>' + )); + } +} diff --git a/src/Mutator/Util/MutatorProfile.php b/src/Mutator/Util/MutatorProfile.php index 04f1e01a1..ba113192e 100644 --- a/src/Mutator/Util/MutatorProfile.php +++ b/src/Mutator/Util/MutatorProfile.php @@ -158,6 +158,7 @@ final class MutatorProfile ]; public const REMOVAL = [ + Mutator\Removal\ArrayItemRemoval::class, Mutator\Removal\FunctionCallRemoval::class, Mutator\Removal\MethodCallRemoval::class, ]; @@ -327,6 +328,7 @@ final class MutatorProfile 'PregMatchMatches' => Mutator\Regex\PregMatchMatches::class, //Removal + 'ArrayItemRemoval' => Mutator\Removal\ArrayItemRemoval::class, 'FunctionCallRemoval' => Mutator\Removal\FunctionCallRemoval::class, 'MethodCallRemoval' => Mutator\Removal\MethodCallRemoval::class, diff --git a/tests/Mutator/Removal/ArrayItemRemovalTest.php b/tests/Mutator/Removal/ArrayItemRemovalTest.php new file mode 100644 index 000000000..9b3e3a950 --- /dev/null +++ b/tests/Mutator/Removal/ArrayItemRemovalTest.php @@ -0,0 +1,165 @@ +doTest($input, $expected, $settings); + } + + public function provideMutationCases(): Generator + { + yield 'It does not mutate empty arrays' => [ + ' [ + ' [ + ' ['remove' => 'last']], + ]; + + yield 'It removes every item on by one when set to `all`' => [ + ' ['remove' => 'all']], + ]; + + yield 'It obeys limit when mutating arrays in `all` mode' => [ + ' ['remove' => 'all', 'limit' => 2]], + ]; + + yield 'It mutates arrays having required items count when removing `all` items' => [ + ' ['remove' => 'all', 'limit' => 2]], + ]; + + yield 'It mutates correctly for limit value (1)' => [ + ' ['remove' => 'all', 'limit' => 1]], + ]; + } + + /** + * @dataProvider provideInvalidConfigurationCases + */ + public function test_settings_validation(string $setting, $value, string $valueInError): void + { + $this->expectException(InvalidConfigException::class); + $this->expectExceptionMessage(sprintf( + 'Invalid configuration of ArrayItemRemoval mutator. Setting `%s` is invalid (%s)', + $setting, + $valueInError + )); + $this->doTest( + ' [$setting => $value]] + ); + } + + public function provideInvalidConfigurationCases(): Generator + { + yield 'remove is null' => [ + 'remove', null, '', + ]; + + yield 'remove is array' => [ + 'remove', [], '', + ]; + + yield 'remove is not valid' => [ + 'remove', 'INVALID', 'INVALID', + ]; + + yield 'remove is not string' => [ + 'remove', 1, '1', + ]; + + yield 'limit is null' => [ + 'limit', null, '', + ]; + + yield 'limit is array' => [ + 'limit', [], '', + ]; + + yield 'limit is zero' => [ + 'limit', 0, '0', + ]; + + yield 'limit is negative' => [ + 'limit', -1, '-1', + ]; + + yield 'limit is not numeric' => [ + 'limit', 'INVALID', 'INVALID', + ]; + } +}