RemovedImplodeFlexibleParamOrderSniff.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  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. * Passing the `$glue` and `$pieces` parameters to `implode()` in reverse order has
  16. * been deprecated in PHP 7.4.
  17. *
  18. * PHP version 7.4
  19. *
  20. * @link https://www.php.net/manual/en/migration74.deprecated.php#migration74.deprecated.core.implode-reverse-parameters
  21. * @link https://wiki.php.net/rfc/deprecations_php_7_4#implode_parameter_order_mix
  22. * @link https://php.net/manual/en/function.implode.php
  23. *
  24. * @since 9.3.0
  25. */
  26. class RemovedImplodeFlexibleParamOrderSniff extends AbstractFunctionCallParameterSniff
  27. {
  28. /**
  29. * Functions to check for.
  30. *
  31. * @since 9.3.0
  32. *
  33. * @var array
  34. */
  35. protected $targetFunctions = array(
  36. 'implode' => true,
  37. 'join' => true,
  38. );
  39. /**
  40. * List of PHP native constants which should be recognized as text strings.
  41. *
  42. * @since 9.3.0
  43. *
  44. * @var array
  45. */
  46. private $constantStrings = array(
  47. 'DIRECTORY_SEPARATOR' => true,
  48. 'PHP_EOL' => true,
  49. );
  50. /**
  51. * List of PHP native functions which should be recognized as returning an array.
  52. *
  53. * Note: The array_*() functions will always be taken into account.
  54. *
  55. * @since 9.3.0
  56. *
  57. * @var array
  58. */
  59. private $arrayFunctions = array(
  60. 'compact' => true,
  61. 'explode' => true,
  62. 'range' => true,
  63. );
  64. /**
  65. * List of PHP native array functions which should *not* be recognized as returning an array.
  66. *
  67. * @since 9.3.0
  68. *
  69. * @var array
  70. */
  71. private $arrayFunctionExceptions = array(
  72. 'array_key_exists' => true,
  73. 'array_key_first' => true,
  74. 'array_key_last' => true,
  75. 'array_multisort' => true,
  76. 'array_pop' => true,
  77. 'array_product' => true,
  78. 'array_push' => true,
  79. 'array_search' => true,
  80. 'array_shift' => true,
  81. 'array_sum' => true,
  82. 'array_unshift' => true,
  83. 'array_walk_recursive' => true,
  84. 'array_walk' => true,
  85. );
  86. /**
  87. * Do a version check to determine if this sniff needs to run at all.
  88. *
  89. * @since 9.3.0
  90. *
  91. * @return bool
  92. */
  93. protected function bowOutEarly()
  94. {
  95. return ($this->supportsAbove('7.4') === false);
  96. }
  97. /**
  98. * Process the parameters of a matched function.
  99. *
  100. * @since 9.3.0
  101. *
  102. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  103. * @param int $stackPtr The position of the current token in the stack.
  104. * @param string $functionName The token content (function name) which was matched.
  105. * @param array $parameters Array with information about the parameters.
  106. *
  107. * @return int|void Integer stack pointer to skip forward or void to continue
  108. * normal file processing.
  109. */
  110. public function processParameters(File $phpcsFile, $stackPtr, $functionName, $parameters)
  111. {
  112. if (isset($parameters[2]) === false) {
  113. // Only one parameter, this must be $pieces. Bow out.
  114. return;
  115. }
  116. $tokens = $phpcsFile->getTokens();
  117. /*
  118. * Examine the first parameter.
  119. * If there is any indication that this is an array declaration, we have an error.
  120. */
  121. $targetParam = $parameters[1];
  122. $start = $targetParam['start'];
  123. $end = ($targetParam['end'] + 1);
  124. $isOnlyText = true;
  125. $firstNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $start, $end, true);
  126. if ($firstNonEmpty === false) {
  127. // Parse error. Shouldn't be possible.
  128. return;
  129. }
  130. if ($tokens[$firstNonEmpty]['code'] === \T_OPEN_PARENTHESIS) {
  131. $start = ($firstNonEmpty + 1);
  132. $end = $tokens[$firstNonEmpty]['parenthesis_closer'];
  133. }
  134. $hasTernary = $phpcsFile->findNext(\T_INLINE_THEN, $start, $end);
  135. if ($hasTernary !== false
  136. && isset($tokens[$start]['nested_parenthesis'], $tokens[$hasTernary]['nested_parenthesis'])
  137. && count($tokens[$start]['nested_parenthesis']) === count($tokens[$hasTernary]['nested_parenthesis'])
  138. ) {
  139. $start = ($hasTernary + 1);
  140. }
  141. for ($i = $start; $i < $end; $i++) {
  142. $tokenCode = $tokens[$i]['code'];
  143. if (isset(Tokens::$emptyTokens[$tokenCode])) {
  144. continue;
  145. }
  146. if ($tokenCode === \T_STRING && isset($this->constantStrings[$tokens[$i]['content']])) {
  147. continue;
  148. }
  149. if ($hasTernary !== false && $tokenCode === \T_INLINE_ELSE) {
  150. continue;
  151. }
  152. if (isset(Tokens::$stringTokens[$tokenCode]) === false) {
  153. $isOnlyText = false;
  154. }
  155. if ($tokenCode === \T_ARRAY || $tokenCode === \T_OPEN_SHORT_ARRAY || $tokenCode === \T_ARRAY_CAST) {
  156. $this->throwNotice($phpcsFile, $stackPtr, $functionName);
  157. return;
  158. }
  159. if ($tokenCode === \T_STRING) {
  160. /*
  161. * Check for specific functions which return an array (i.e. $pieces).
  162. */
  163. $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($i + 1), $end, true);
  164. if ($nextNonEmpty === false || $tokens[$nextNonEmpty]['code'] !== \T_OPEN_PARENTHESIS) {
  165. continue;
  166. }
  167. $nameLc = strtolower($tokens[$i]['content']);
  168. if (isset($this->arrayFunctions[$nameLc]) === false
  169. && (strpos($nameLc, 'array_') !== 0
  170. || isset($this->arrayFunctionExceptions[$nameLc]) === true)
  171. ) {
  172. continue;
  173. }
  174. // Now make sure it's the PHP native function being called.
  175. $prevNonEmpty = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($i - 1), $start, true);
  176. if ($tokens[$prevNonEmpty]['code'] === \T_DOUBLE_COLON
  177. || $tokens[$prevNonEmpty]['code'] === \T_OBJECT_OPERATOR
  178. ) {
  179. // Method call, not a call to the PHP native function.
  180. continue;
  181. }
  182. if ($tokens[$prevNonEmpty]['code'] === \T_NS_SEPARATOR
  183. && $tokens[$prevNonEmpty - 1]['code'] === \T_STRING
  184. ) {
  185. // Namespaced function.
  186. continue;
  187. }
  188. // Ok, so we know that there is an array function in the first param.
  189. // 99.9% chance that this is $pieces, not $glue.
  190. $this->throwNotice($phpcsFile, $stackPtr, $functionName);
  191. return;
  192. }
  193. }
  194. if ($isOnlyText === true) {
  195. // First parameter only contained text string tokens, i.e. glue.
  196. return;
  197. }
  198. /*
  199. * Examine the second parameter.
  200. */
  201. $targetParam = $parameters[2];
  202. $start = $targetParam['start'];
  203. $end = ($targetParam['end'] + 1);
  204. $firstNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, $start, $end, true);
  205. if ($firstNonEmpty === false) {
  206. // Parse error. Shouldn't be possible.
  207. return;
  208. }
  209. if ($tokens[$firstNonEmpty]['code'] === \T_OPEN_PARENTHESIS) {
  210. $start = ($firstNonEmpty + 1);
  211. $end = $tokens[$firstNonEmpty]['parenthesis_closer'];
  212. }
  213. $hasTernary = $phpcsFile->findNext(\T_INLINE_THEN, $start, $end);
  214. if ($hasTernary !== false
  215. && isset($tokens[$start]['nested_parenthesis'], $tokens[$hasTernary]['nested_parenthesis'])
  216. && count($tokens[$start]['nested_parenthesis']) === count($tokens[$hasTernary]['nested_parenthesis'])
  217. ) {
  218. $start = ($hasTernary + 1);
  219. }
  220. for ($i = $start; $i < $end; $i++) {
  221. $tokenCode = $tokens[$i]['code'];
  222. if (isset(Tokens::$emptyTokens[$tokenCode])) {
  223. continue;
  224. }
  225. if ($tokenCode === \T_ARRAY || $tokenCode === \T_OPEN_SHORT_ARRAY || $tokenCode === \T_ARRAY_CAST) {
  226. // Found an array, $pieces is second.
  227. return;
  228. }
  229. if ($tokenCode === \T_STRING && isset($this->constantStrings[$tokens[$i]['content']])) {
  230. // One of the special cased, PHP native string constants found.
  231. $this->throwNotice($phpcsFile, $stackPtr, $functionName);
  232. return;
  233. }
  234. if ($tokenCode === \T_STRING || $tokenCode === \T_VARIABLE) {
  235. // Function call, constant or variable encountered.
  236. // No matter what this is combined with, we won't be able to reliably determine the value.
  237. return;
  238. }
  239. if ($tokenCode === \T_CONSTANT_ENCAPSED_STRING
  240. || $tokenCode === \T_DOUBLE_QUOTED_STRING
  241. || $tokenCode === \T_HEREDOC
  242. || $tokenCode === \T_NOWDOC
  243. ) {
  244. $this->throwNotice($phpcsFile, $stackPtr, $functionName);
  245. return;
  246. }
  247. }
  248. }
  249. /**
  250. * Throw the error/warning.
  251. *
  252. * @since 9.3.0
  253. *
  254. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  255. * @param int $stackPtr The position of the current token in the stack.
  256. * @param string $functionName The token content (function name) which was matched.
  257. *
  258. * @return void
  259. */
  260. protected function throwNotice(File $phpcsFile, $stackPtr, $functionName)
  261. {
  262. $message = 'Passing the $glue and $pieces parameters in reverse order to %s has been deprecated since PHP 7.4';
  263. $isError = false;
  264. $errorCode = 'Deprecated';
  265. $data = array($functionName);
  266. /*
  267. Support for the deprecated behaviour is expected to be removed in PHP 8.0.
  268. Once this has been implemented, this section should be uncommented.
  269. if ($this->supportsAbove('8.0') === true) {
  270. $message .= ' and is removed since PHP 8.0';
  271. $isError = true;
  272. $errorCode = 'Removed';
  273. }
  274. */
  275. $message .= '; $glue should be the first parameter and $pieces the second';
  276. $this->addMessage($phpcsFile, $message, $stackPtr, $isError, $errorCode, $data);
  277. }
  278. }