MbStrFunctionsFixer.php 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. <?php
  2. /*
  3. * This file is part of PHP CS Fixer.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  7. *
  8. * This source file is subject to the MIT license that is bundled
  9. * with this source code in the file LICENSE.
  10. */
  11. namespace PhpCsFixer\Fixer\Alias;
  12. use PhpCsFixer\AbstractFunctionReferenceFixer;
  13. use PhpCsFixer\FixerDefinition\CodeSample;
  14. use PhpCsFixer\FixerDefinition\FixerDefinition;
  15. use PhpCsFixer\Tokenizer\Analyzer\ArgumentsAnalyzer;
  16. use PhpCsFixer\Tokenizer\Token;
  17. use PhpCsFixer\Tokenizer\Tokens;
  18. /**
  19. * @author Filippo Tessarotto <zoeslam@gmail.com>
  20. */
  21. final class MbStrFunctionsFixer extends AbstractFunctionReferenceFixer
  22. {
  23. /**
  24. * @var array the list of the string-related function names and their mb_ equivalent
  25. */
  26. private static $functionsMap = [
  27. 'str_split' => ['alternativeName' => 'mb_str_split', 'argumentCount' => [1, 2, 3]],
  28. 'stripos' => ['alternativeName' => 'mb_stripos', 'argumentCount' => [2, 3]],
  29. 'stristr' => ['alternativeName' => 'mb_stristr', 'argumentCount' => [2, 3]],
  30. 'strlen' => ['alternativeName' => 'mb_strlen', 'argumentCount' => [1]],
  31. 'strpos' => ['alternativeName' => 'mb_strpos', 'argumentCount' => [2, 3]],
  32. 'strrchr' => ['alternativeName' => 'mb_strrchr', 'argumentCount' => [2]],
  33. 'strripos' => ['alternativeName' => 'mb_strripos', 'argumentCount' => [2, 3]],
  34. 'strrpos' => ['alternativeName' => 'mb_strrpos', 'argumentCount' => [2, 3]],
  35. 'strstr' => ['alternativeName' => 'mb_strstr', 'argumentCount' => [2, 3]],
  36. 'strtolower' => ['alternativeName' => 'mb_strtolower', 'argumentCount' => [1]],
  37. 'strtoupper' => ['alternativeName' => 'mb_strtoupper', 'argumentCount' => [1]],
  38. 'substr' => ['alternativeName' => 'mb_substr', 'argumentCount' => [2, 3]],
  39. 'substr_count' => ['alternativeName' => 'mb_substr_count', 'argumentCount' => [2, 3, 4]],
  40. ];
  41. /**
  42. * @var array<string, array>
  43. */
  44. private $functions;
  45. public function __construct()
  46. {
  47. parent::__construct();
  48. $this->functions = array_filter(
  49. self::$functionsMap,
  50. static function (array $mapping) {
  51. return \function_exists($mapping['alternativeName']) && (new \ReflectionFunction($mapping['alternativeName']))->isInternal();
  52. }
  53. );
  54. }
  55. /**
  56. * {@inheritdoc}
  57. */
  58. public function getDefinition()
  59. {
  60. return new FixerDefinition(
  61. 'Replace non multibyte-safe functions with corresponding mb function.',
  62. [
  63. new CodeSample(
  64. '<?php
  65. $a = strlen($a);
  66. $a = strpos($a, $b);
  67. $a = strrpos($a, $b);
  68. $a = substr($a, $b);
  69. $a = strtolower($a);
  70. $a = strtoupper($a);
  71. $a = stripos($a, $b);
  72. $a = strripos($a, $b);
  73. $a = strstr($a, $b);
  74. $a = stristr($a, $b);
  75. $a = strrchr($a, $b);
  76. $a = substr_count($a, $b);
  77. '
  78. ),
  79. ],
  80. null,
  81. 'Risky when any of the functions are overridden.'
  82. );
  83. }
  84. /**
  85. * {@inheritdoc}
  86. */
  87. public function isCandidate(Tokens $tokens)
  88. {
  89. return $tokens->isTokenKindFound(T_STRING);
  90. }
  91. /**
  92. * {@inheritdoc}
  93. */
  94. protected function applyFix(\SplFileInfo $file, Tokens $tokens)
  95. {
  96. $argumentsAnalyzer = new ArgumentsAnalyzer();
  97. foreach ($this->functions as $functionIdentity => $functionReplacement) {
  98. $currIndex = 0;
  99. while (null !== $currIndex) {
  100. // try getting function reference and translate boundaries for humans
  101. $boundaries = $this->find($functionIdentity, $tokens, $currIndex, $tokens->count() - 1);
  102. if (null === $boundaries) {
  103. // next function search, as current one not found
  104. continue 2;
  105. }
  106. list($functionName, $openParenthesis, $closeParenthesis) = $boundaries;
  107. $count = $argumentsAnalyzer->countArguments($tokens, $openParenthesis, $closeParenthesis);
  108. if (!\in_array($count, $functionReplacement['argumentCount'], true)) {
  109. continue 2;
  110. }
  111. // analysing cursor shift, so nested calls could be processed
  112. $currIndex = $openParenthesis;
  113. $tokens[$functionName] = new Token([T_STRING, $functionReplacement['alternativeName']]);
  114. }
  115. }
  116. }
  117. }