Skip to content

Commit

Permalink
Mutator/mbstring (#664)
Browse files Browse the repository at this point in the history
* #654 mbstring mutator

* #654 mbstring mutator configuration

* #654 fix code analysis issues

* #654 update json scheme for MBString mutator

* #654 fix spaceing

* #654 remove mutants

* #654 drop ereg functions in favoure of preg functions

* #654 fix styles

* #654 drop support fro mb_ereg* functions

* #654 mutate mb_convert_case with integer mode

* #654 remove functions that cannot be easily mapped

* #654 rm mb_split from json schema

* #654 rm mb_strrichr from json schema

* #654 code style fixes

* #654 simplify MBString mutator

* #654 remove getFunctionName fx

* #654 add tests cases for capitalization

* #654 add test for calling functions via variable
  • Loading branch information
majkel89 authored and maks-rafalko committed Apr 1, 2019
1 parent 16958e6 commit e25b9d6
Show file tree
Hide file tree
Showing 4 changed files with 878 additions and 0 deletions.
75 changes: 75 additions & 0 deletions resources/schema.json
Expand Up @@ -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"
}
}
}
}
}
]
}
}
}
},
Expand Down
196 changes: 196 additions & 0 deletions src/Mutator/Extensions/MBString.php
@@ -0,0 +1,196 @@
<?php
/**
* This code is licensed under the BSD 3-Clause License.
*
* Copyright (c) 2017-2019, Maks Rafalko
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

declare(strict_types=1);

namespace Infection\Mutator\Extensions;

use Generator;
use Infection\Mutator\Util\Mutator;
use Infection\Mutator\Util\MutatorConfig;
use PhpParser\Node;

/**
* @internal
*/
final class MBString extends Mutator
{
private $converters;

public function __construct(MutatorConfig $config)
{
parent::__construct($config);

$settings = $this->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()
);
}
}
9 changes: 9 additions & 0 deletions src/Mutator/Util/MutatorProfile.php
Expand Up @@ -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,
Expand Down Expand Up @@ -232,6 +233,10 @@ final class MutatorProfile
Mutator\Unwrap\UnwrapUcWords::class,
];

public const EXTENSIONS = [
Mutator\Extensions\MBString::class,
];

public const DEFAULT = [
'@arithmetic',
'@boolean',
Expand All @@ -246,6 +251,7 @@ final class MutatorProfile
'@return_value',
'@sort',
'@zero_iteration',
'@extensions',
];

public const FULL_MUTATOR_LIST = [
Expand Down Expand Up @@ -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,
];
}

0 comments on commit e25b9d6

Please sign in to comment.