RemovedPCREModifiersSniff.php 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. <?php
  2. /**
  3. * PHPCompatibility, an external standard for PHP_CodeSniffer.
  4. *
  5. * @package PHPCompatibility
  6. * @copyright 2012-2019 PHPCompatibility Contributors
  7. * @license https://opensource.org/licenses/LGPL-3.0 LGPL3
  8. * @link https://github.com/PHPCompatibility/PHPCompatibility
  9. */
  10. namespace PHPCompatibility\Sniffs\ParameterValues;
  11. use PHPCompatibility\AbstractFunctionCallParameterSniff;
  12. use PHP_CodeSniffer_File as File;
  13. use PHP_CodeSniffer_Tokens as Tokens;
  14. /**
  15. * Check for the use of deprecated and removed regex modifiers for PCRE regex functions.
  16. *
  17. * Initially just checks for the `e` modifier, which was deprecated since PHP 5.5
  18. * and removed as of PHP 7.0.
  19. *
  20. * {@internal If and when this sniff would need to start checking for other modifiers, a minor
  21. * refactor will be needed as all references to the `e` modifier are currently hard-coded.}
  22. *
  23. * PHP version 5.5
  24. * PHP version 7.0
  25. *
  26. * @link https://wiki.php.net/rfc/remove_preg_replace_eval_modifier
  27. * @link https://wiki.php.net/rfc/remove_deprecated_functionality_in_php7
  28. * @link https://www.php.net/manual/en/reference.pcre.pattern.modifiers.php
  29. *
  30. * @since 5.6
  31. * @since 7.0.8 This sniff now throws a warning (deprecated) or an error (removed) depending
  32. * on the `testVersion` set. Previously it would always throw an error.
  33. * @since 8.2.0 Now extends the `AbstractFunctionCallParameterSniff` instead of the base `Sniff` class.
  34. * @since 9.0.0 Renamed from `PregReplaceEModifierSniff` to `RemovedPCREModifiersSniff`.
  35. */
  36. class RemovedPCREModifiersSniff extends AbstractFunctionCallParameterSniff
  37. {
  38. /**
  39. * Functions to check for.
  40. *
  41. * @since 7.0.1
  42. * @since 8.2.0 Renamed from `$functions` to `$targetFunctions`.
  43. *
  44. * @var array
  45. */
  46. protected $targetFunctions = array(
  47. 'preg_replace' => true,
  48. 'preg_filter' => true,
  49. );
  50. /**
  51. * Regex bracket delimiters.
  52. *
  53. * @since 7.0.5 This array was originally contained within the `process()` method.
  54. *
  55. * @var array
  56. */
  57. protected $doublesSeparators = array(
  58. '{' => '}',
  59. '[' => ']',
  60. '(' => ')',
  61. '<' => '>',
  62. );
  63. /**
  64. * Process the parameters of a matched function.
  65. *
  66. * @since 5.6
  67. * @since 8.2.0 Renamed from `process()` to `processParameters()` and removed
  68. * logic superfluous now the sniff extends the abstract.
  69. *
  70. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  71. * @param int $stackPtr The position of the current token in the stack.
  72. * @param string $functionName The token content (function name) which was matched.
  73. * @param array $parameters Array with information about the parameters.
  74. *
  75. * @return int|void Integer stack pointer to skip forward or void to continue
  76. * normal file processing.
  77. */
  78. public function processParameters(File $phpcsFile, $stackPtr, $functionName, $parameters)
  79. {
  80. // Check the first parameter in the function call as that should contain the regex(es).
  81. if (isset($parameters[1]) === false) {
  82. return;
  83. }
  84. $tokens = $phpcsFile->getTokens();
  85. $functionNameLc = strtolower($functionName);
  86. $firstParam = $parameters[1];
  87. // Differentiate between an array of patterns passed and a single pattern.
  88. $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $firstParam['start'], ($firstParam['end'] + 1), true);
  89. if ($nextNonEmpty !== false && ($tokens[$nextNonEmpty]['code'] === \T_ARRAY || $tokens[$nextNonEmpty]['code'] === \T_OPEN_SHORT_ARRAY)) {
  90. $arrayValues = $this->getFunctionCallParameters($phpcsFile, $nextNonEmpty);
  91. if ($functionNameLc === 'preg_replace_callback_array') {
  92. // For preg_replace_callback_array(), the patterns will be in the array keys.
  93. foreach ($arrayValues as $value) {
  94. $hasKey = $phpcsFile->findNext(\T_DOUBLE_ARROW, $value['start'], ($value['end'] + 1));
  95. if ($hasKey === false) {
  96. continue;
  97. }
  98. $value['end'] = ($hasKey - 1);
  99. $value['raw'] = trim($phpcsFile->getTokensAsString($value['start'], ($hasKey - $value['start'])));
  100. $this->processRegexPattern($value, $phpcsFile, $value['end'], $functionName);
  101. }
  102. } else {
  103. // Otherwise, the patterns will be in the array values.
  104. foreach ($arrayValues as $value) {
  105. $hasKey = $phpcsFile->findNext(\T_DOUBLE_ARROW, $value['start'], ($value['end'] + 1));
  106. if ($hasKey !== false) {
  107. $value['start'] = ($hasKey + 1);
  108. $value['raw'] = trim($phpcsFile->getTokensAsString($value['start'], (($value['end'] + 1) - $value['start'])));
  109. }
  110. $this->processRegexPattern($value, $phpcsFile, $value['end'], $functionName);
  111. }
  112. }
  113. } else {
  114. $this->processRegexPattern($firstParam, $phpcsFile, $stackPtr, $functionName);
  115. }
  116. }
  117. /**
  118. * Do a version check to determine if this sniff needs to run at all.
  119. *
  120. * @since 8.2.0
  121. *
  122. * @return bool
  123. */
  124. protected function bowOutEarly()
  125. {
  126. return ($this->supportsAbove('5.5') === false);
  127. }
  128. /**
  129. * Analyse a potential regex pattern for use of the /e modifier.
  130. *
  131. * @since 7.1.2 This logic was originally contained within the `process()` method.
  132. *
  133. * @param array $pattern Array containing the start and end token
  134. * pointer of the potential regex pattern and
  135. * the raw string value of the pattern.
  136. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  137. * @param int $stackPtr The position of the current token in the
  138. * stack passed in $tokens.
  139. * @param string $functionName The function which contained the pattern.
  140. *
  141. * @return void
  142. */
  143. protected function processRegexPattern($pattern, File $phpcsFile, $stackPtr, $functionName)
  144. {
  145. $tokens = $phpcsFile->getTokens();
  146. /*
  147. * The pattern might be build up of a combination of strings, variables
  148. * and function calls. We are only concerned with the strings.
  149. */
  150. $regex = '';
  151. for ($i = $pattern['start']; $i <= $pattern['end']; $i++) {
  152. if (isset(Tokens::$stringTokens[$tokens[$i]['code']]) === true) {
  153. $content = $this->stripQuotes($tokens[$i]['content']);
  154. if ($tokens[$i]['code'] === \T_DOUBLE_QUOTED_STRING) {
  155. $content = $this->stripVariables($content);
  156. }
  157. $regex .= trim($content);
  158. }
  159. }
  160. // Deal with multi-line regexes which were broken up in several string tokens.
  161. if ($tokens[$pattern['start']]['line'] !== $tokens[$pattern['end']]['line']) {
  162. $regex = $this->stripQuotes($regex);
  163. }
  164. if ($regex === '') {
  165. // No string token found in the first parameter, so skip it (e.g. if variable passed in).
  166. return;
  167. }
  168. $regexFirstChar = substr($regex, 0, 1);
  169. // Make sure that the character identified as the delimiter is valid.
  170. // Otherwise, it is a false positive caused by the string concatenation.
  171. if (preg_match('`[a-z0-9\\\\ ]`i', $regexFirstChar) === 1) {
  172. return;
  173. }
  174. if (isset($this->doublesSeparators[$regexFirstChar])) {
  175. $regexEndPos = strrpos($regex, $this->doublesSeparators[$regexFirstChar]);
  176. } else {
  177. $regexEndPos = strrpos($regex, $regexFirstChar);
  178. }
  179. if ($regexEndPos !== false) {
  180. $modifiers = substr($regex, $regexEndPos + 1);
  181. $this->examineModifiers($phpcsFile, $stackPtr, $functionName, $modifiers);
  182. }
  183. }
  184. /**
  185. * Examine the regex modifier string.
  186. *
  187. * @since 8.2.0 Split off from the `processRegexPattern()` method.
  188. *
  189. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  190. * @param int $stackPtr The position of the current token in the
  191. * stack passed in $tokens.
  192. * @param string $functionName The function which contained the pattern.
  193. * @param string $modifiers The regex modifiers found.
  194. *
  195. * @return void
  196. */
  197. protected function examineModifiers(File $phpcsFile, $stackPtr, $functionName, $modifiers)
  198. {
  199. if (strpos($modifiers, 'e') !== false) {
  200. $error = '%s() - /e modifier is deprecated since PHP 5.5';
  201. $isError = false;
  202. $errorCode = 'Deprecated';
  203. $data = array($functionName);
  204. if ($this->supportsAbove('7.0')) {
  205. $error .= ' and removed since PHP 7.0';
  206. $isError = true;
  207. $errorCode = 'Removed';
  208. }
  209. $this->addMessage($phpcsFile, $error, $stackPtr, $isError, $errorCode, $data);
  210. }
  211. }
  212. }