123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- <?php
- declare(strict_types=1);
- /*
- * This file is part of PHP CS Fixer.
- *
- * (c) Fabien Potencier <fabien@symfony.com>
- * Dariusz Rumiński <dariusz.ruminski@gmail.com>
- *
- * This source file is subject to the MIT license that is bundled
- * with this source code in the file LICENSE.
- */
- namespace PhpCsFixer\Fixer\Alias;
- use PhpCsFixer\AbstractFixer;
- use PhpCsFixer\FixerDefinition\CodeSample;
- use PhpCsFixer\FixerDefinition\FixerDefinition;
- use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
- use PhpCsFixer\Preg;
- use PhpCsFixer\PregException;
- use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
- use PhpCsFixer\Tokenizer\Token;
- use PhpCsFixer\Tokenizer\Tokens;
- /**
- * @author Matteo Beccati <matteo@beccati.com>
- */
- final class EregToPregFixer extends AbstractFixer
- {
- /**
- * @var list<array<int, string>> the list of the ext/ereg function names, their preg equivalent and the preg modifier(s), if any
- * all condensed in an array of arrays
- */
- private static array $functions = [
- ['ereg', 'preg_match', ''],
- ['eregi', 'preg_match', 'i'],
- ['ereg_replace', 'preg_replace', ''],
- ['eregi_replace', 'preg_replace', 'i'],
- ['split', 'preg_split', ''],
- ['spliti', 'preg_split', 'i'],
- ];
- /**
- * @var list<string> the list of preg delimiters, in order of preference
- */
- private static array $delimiters = ['/', '#', '!'];
- /**
- * {@inheritdoc}
- */
- public function getDefinition(): FixerDefinitionInterface
- {
- return new FixerDefinition(
- 'Replace deprecated `ereg` regular expression functions with `preg`.',
- [new CodeSample("<?php \$x = ereg('[A-Z]');\n")],
- null,
- 'Risky if the `ereg` function is overridden.'
- );
- }
- /**
- * {@inheritdoc}
- *
- * Must run after NoUselessConcatOperatorFixer.
- */
- public function getPriority(): int
- {
- return 0;
- }
- /**
- * {@inheritdoc}
- */
- public function isCandidate(Tokens $tokens): bool
- {
- return $tokens->isTokenKindFound(T_STRING);
- }
- /**
- * {@inheritdoc}
- */
- public function isRisky(): bool
- {
- return true;
- }
- /**
- * {@inheritdoc}
- */
- protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
- {
- $end = $tokens->count() - 1;
- $functionsAnalyzer = new FunctionsAnalyzer();
- foreach (self::$functions as $map) {
- // the sequence is the function name, followed by "(" and a quoted string
- $seq = [[T_STRING, $map[0]], '(', [T_CONSTANT_ENCAPSED_STRING]];
- $currIndex = 0;
- while (true) {
- $match = $tokens->findSequence($seq, $currIndex, $end, false);
- // did we find a match?
- if (null === $match) {
- break;
- }
- // findSequence also returns the tokens, but we're only interested in the indices, i.e.:
- // 0 => function name,
- // 1 => bracket "("
- // 2 => quoted string passed as 1st parameter
- $match = array_keys($match);
- // advance tokenizer cursor
- $currIndex = $match[2];
- if (!$functionsAnalyzer->isGlobalFunctionCall($tokens, $match[0])) {
- continue;
- }
- // ensure the first parameter is just a string (e.g. has nothing appended)
- $next = $tokens->getNextMeaningfulToken($match[2]);
- if (null === $next || !$tokens[$next]->equalsAny([',', ')'])) {
- continue;
- }
- // convert to PCRE
- $regexTokenContent = $tokens[$match[2]]->getContent();
- if ('b' === $regexTokenContent[0] || 'B' === $regexTokenContent[0]) {
- $quote = $regexTokenContent[1];
- $prefix = $regexTokenContent[0];
- $string = substr($regexTokenContent, 2, -1);
- } else {
- $quote = $regexTokenContent[0];
- $prefix = '';
- $string = substr($regexTokenContent, 1, -1);
- }
- $delim = $this->getBestDelimiter($string);
- $preg = $delim.addcslashes($string, $delim).$delim.'D'.$map[2];
- // check if the preg is valid
- if (!$this->checkPreg($preg)) {
- continue;
- }
- // modify function and argument
- $tokens[$match[0]] = new Token([T_STRING, $map[1]]);
- $tokens[$match[2]] = new Token([T_CONSTANT_ENCAPSED_STRING, $prefix.$quote.$preg.$quote]);
- }
- }
- }
- /**
- * Check the validity of a PCRE.
- *
- * @param string $pattern the regular expression
- */
- private function checkPreg(string $pattern): bool
- {
- try {
- Preg::match($pattern, '');
- return true;
- } catch (PregException $e) {
- return false;
- }
- }
- /**
- * Get the delimiter that would require the least escaping in a regular expression.
- *
- * @param string $pattern the regular expression
- *
- * @return string the preg delimiter
- */
- private function getBestDelimiter(string $pattern): string
- {
- // try to find something that's not used
- $delimiters = [];
- foreach (self::$delimiters as $k => $d) {
- if (!str_contains($pattern, $d)) {
- return $d;
- }
- $delimiters[$d] = [substr_count($pattern, $d), $k];
- }
- // return the least used delimiter, using the position in the list as a tiebreaker
- uasort($delimiters, static function (array $a, array $b): int {
- if ($a[0] === $b[0]) {
- return $a[1] <=> $b[1];
- }
- return $a[0] <=> $b[0];
- });
- return key($delimiters);
- }
- }
|