diff --git a/resources/schema.json b/resources/schema.json index 5a37a82f4..14b93a998 100644 --- a/resources/schema.json +++ b/resources/schema.json @@ -144,6 +144,81 @@ } ] } + }, + "MBString": { + "type": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "settings": { + "type": "object", + "additionalProperties": false, + "properties": { + "mb_chr": { + "type": "boolean" + }, + "mb_ord": { + "type": "boolean" + }, + "mb_parse_str": { + "type": "boolean" + }, + "mb_send_mail": { + "type": "boolean" + }, + "mb_strcut": { + "type": "boolean" + }, + "mb_stripos": { + "type": "boolean" + }, + "mb_stristr": { + "type": "boolean" + }, + "mb_strlen": { + "type": "boolean" + }, + "mb_strpos": { + "type": "boolean" + }, + "mb_strrchr": { + "type": "boolean" + }, + "mb_strripos": { + "type": "boolean" + }, + "mb_strrpos": { + "type": "boolean" + }, + "mb_strstr": { + "type": "boolean" + }, + "mb_strtolower": { + "type": "boolean" + }, + "mb_strtoupper": { + "type": "boolean" + }, + "mb_substr_count": { + "type": "boolean" + }, + "mb_substr": { + "type": "boolean" + }, + "mb_convert_case": { + "type": "boolean" + } + } + } + } + } + ] + } } } }, diff --git a/src/Mutator/Extensions/MBString.php b/src/Mutator/Extensions/MBString.php new file mode 100644 index 000000000..c10fc4417 --- /dev/null +++ b/src/Mutator/Extensions/MBString.php @@ -0,0 +1,196 @@ +getSettings(); + + $this->setupConverters($settings); + } + + /** + * @param Node\Expr\FuncCall $node + * + * @return Node|Node[]|Generator + */ + public function mutate(Node $node) + { + yield from $this->converters[$node->name->toLowerString()]($node); + } + + protected function mutatesNode(Node $node): bool + { + if (!$node instanceof Node\Expr\FuncCall || !$node->name instanceof Node\Name) { + return false; + } + + return isset($this->converters[$node->name->toLowerString()]); + } + + private function setupConverters(array $functionsMap): void + { + $converters = [ + 'mb_chr' => $this->mapFunctionAndRemoveExtraArgs('chr', 1), + 'mb_ord' => $this->mapFunctionAndRemoveExtraArgs('ord', 1), + 'mb_parse_str' => $this->mapFunction('parse_str'), + 'mb_send_mail' => $this->mapFunction('mail'), + 'mb_strcut' => $this->mapFunctionAndRemoveExtraArgs('substr', 3), + 'mb_stripos' => $this->mapFunctionAndRemoveExtraArgs('stripos', 3), + 'mb_stristr' => $this->mapFunctionAndRemoveExtraArgs('stristr', 3), + 'mb_strlen' => $this->mapFunctionAndRemoveExtraArgs('strlen', 1), + 'mb_strpos' => $this->mapFunctionAndRemoveExtraArgs('strpos', 3), + 'mb_strrchr' => $this->mapFunctionAndRemoveExtraArgs('strrchr', 2), + 'mb_strripos' => $this->mapFunctionAndRemoveExtraArgs('strripos', 3), + 'mb_strrpos' => $this->mapFunctionAndRemoveExtraArgs('strrpos', 3), + 'mb_strstr' => $this->mapFunctionAndRemoveExtraArgs('strstr', 3), + 'mb_strtolower' => $this->mapFunctionAndRemoveExtraArgs('strtolower', 1), + 'mb_strtoupper' => $this->mapFunctionAndRemoveExtraArgs('strtoupper', 1), + 'mb_substr_count' => $this->mapFunctionAndRemoveExtraArgs('substr_count', 2), + 'mb_substr' => $this->mapFunctionAndRemoveExtraArgs('substr', 3), + 'mb_convert_case' => $this->mapConvertCase(), + ]; + + $functionsToRemove = \array_filter($functionsMap, static function ($isOn) { + return !$isOn; + }); + + $this->converters = \array_diff_key($converters, $functionsToRemove); + } + + private function mapFunction(string $newFunctionName): callable + { + return function (Node\Expr\FuncCall $node) use ($newFunctionName): Generator { + yield $this->mapFunctionCall($node, $newFunctionName, $node->args); + }; + } + + private function mapFunctionAndRemoveExtraArgs(string $newFunctionName, int $argsAtMost): callable + { + return function (Node\Expr\FuncCall $node) use ($newFunctionName, $argsAtMost): Generator { + yield $this->mapFunctionCall($node, $newFunctionName, \array_slice($node->args, 0, $argsAtMost)); + }; + } + + private function mapConvertCase(): callable + { + return function (Node\Expr\FuncCall $node): Generator { + $modeValue = $this->getConvertCaseModeValue($node); + + if ($modeValue === null) { + return; + } + + $functionName = $this->getConvertCaseFunctionName($modeValue); + + if ($functionName === null) { + return; + } + + yield $this->mapFunctionCall($node, $functionName, [$node->args[0]]); + }; + } + + private function getConvertCaseModeValue(Node\Expr\FuncCall $node): ?int + { + if (\count($node->args) < 2) { + return null; + } + + $mode = $node->args[1]->value; + + if ($mode instanceof Node\Scalar\LNumber) { + return $mode->value; + } + + if ($mode instanceof Node\Expr\ConstFetch) { + return \constant($mode->name->toString()); + } + + return null; + } + + private function getConvertCaseFunctionName(int $mode): ?string + { + if ($this->isInMbCaseMode($mode, 'MB_CASE_UPPER', 'MB_CASE_UPPER_SIMPLE')) { + return 'strtoupper'; + } + + if ($this->isInMbCaseMode($mode, 'MB_CASE_LOWER', 'MB_CASE_LOWER_SIMPLE', 'MB_CASE_FOLD', 'MB_CASE_FOLD_SIMPLE')) { + return 'strtolower'; + } + + if ($this->isInMbCaseMode($mode, 'MB_CASE_TITLE', 'MB_CASE_TITLE_SIMPLE')) { + return 'ucwords'; + } + + return null; + } + + private function isInMbCaseMode(int $mode, string ...$cases): bool + { + foreach ($cases as $constant) { + if (\defined($constant) && \constant($constant) === $mode) { + return true; + } + } + + return false; + } + + private function mapFunctionCall(Node\Expr\FuncCall $node, string $newFuncName, array $args): Node\Expr\FuncCall + { + return new Node\Expr\FuncCall( + new Node\Name($newFuncName, $node->name->getAttributes()), + $args, + $node->getAttributes() + ); + } +} diff --git a/src/Mutator/Util/MutatorProfile.php b/src/Mutator/Util/MutatorProfile.php index ba113192e..a8f46427c 100644 --- a/src/Mutator/Util/MutatorProfile.php +++ b/src/Mutator/Util/MutatorProfile.php @@ -60,6 +60,7 @@ final class MutatorProfile '@zero_iteration' => self::ZERO_ITERATION, '@cast' => self::CAST, '@unwrap' => self::UNWRAP, + '@extensions' => self::EXTENSIONS, //Special Profiles '@default' => self::DEFAULT, @@ -232,6 +233,10 @@ final class MutatorProfile Mutator\Unwrap\UnwrapUcWords::class, ]; + public const EXTENSIONS = [ + Mutator\Extensions\MBString::class, + ]; + public const DEFAULT = [ '@arithmetic', '@boolean', @@ -246,6 +251,7 @@ final class MutatorProfile '@return_value', '@sort', '@zero_iteration', + '@extensions', ]; public const FULL_MUTATOR_LIST = [ @@ -395,5 +401,8 @@ final class MutatorProfile 'UnwrapTrim' => Mutator\Unwrap\UnwrapTrim::class, 'UnwrapUcFirst' => Mutator\Unwrap\UnwrapUcFirst::class, 'UnwrapUcWords' => Mutator\Unwrap\UnwrapUcWords::class, + + // Extensions + 'MBString' => Mutator\Extensions\MBString::class, ]; } diff --git a/tests/Mutator/Extensions/MBStringTest.php b/tests/Mutator/Extensions/MBStringTest.php new file mode 100644 index 000000000..6a05a4f62 --- /dev/null +++ b/tests/Mutator/Extensions/MBStringTest.php @@ -0,0 +1,598 @@ +doTest($input, $expected, $settings); + } + + public function provideMutationCases(): Generator + { + yield 'It converts mb_strlen with leading slash' => [ + " [ + " [ + " [ + " ['mb_strlen' => true]], + ]; + + yield 'It does not convert mb_strlen when disabled' => [ + " ['mb_strlen' => false]], + ]; + + yield from $this->provideMutationCasesForChr(); + + yield from $this->provideMutationCasesForOrd(); + + yield from $this->provideMutationCasesForParseStr(); + + yield from $this->provideMutationCasesForSendMail(); + + yield from $this->provideMutationCasesForStrCut(); + + yield from $this->provideMutationCasesForStrPos(); + + yield from $this->provideMutationCasesForStrIPos(); + + yield from $this->provideMutationCasesForStrIStr(); + + yield from $this->provideMutationCasesForStrRiPos(); + + yield from $this->provideMutationCasesForStrRPos(); + + yield from $this->provideMutationCasesForStrStr(); + + yield from $this->provideMutationCasesForStrToLower(); + + yield from $this->provideMutationCasesForStrToUpper(); + + yield from $this->provideMutationCasesForSubStrCount(); + + yield from $this->provideMutationCasesForSubStr(); + + yield from $this->provideMutationCasesForStrRChr(); + + yield from $this->provideMutationCasesForConvertCase(); + } + + private function provideMutationCasesForChr(): Generator + { + yield 'It converts mb_chr to chr' => [ + ' [ + ' [ + " [ + ' [ + " [ + " [ + " [ + ' [ + " [ + " [ + " [ + ' [ + " [ + " [ + " [ + ' [ + " [ + " [ + " [ + " [ + ' [ + " [ + " [ + " [ + " [ + ' [ + " [ + " [ + " [ + " [ + ' [ + " [ + " [ + " [ + " [ + ' [ + " [ + " [ + " [ + " [ + ' [ + " [ + " [ + " [ + " [ + ' [ + " [ + " [ + " [ + " [ + ' [ + " [ + " [ + " [ + ' [ + " [ + " [ + " [ + ' [ + " [ + " [ + " [ + ' [ + " [ + " [ + " [ + " [ + ' [ + " [ + " [ + " [ + " [ + ' [ + " [ + " [ + " [ + " [ + " [ + " [ + " [ + " [ + " [ + " [ + " [ + " [ + ' 3, + 'MB_CASE_UPPER_SIMPLE' => 4, + 'MB_CASE_LOWER_SIMPLE' => 5, + 'MB_CASE_TITLE_SIMPLE' => 6, + 'MB_CASE_FOLD_SIMPLE' => 7, + ] as $constantName => $constantValue) { + if (!\defined($constantName)) { + \define($constantName, $constantValue); + } + } + } +}