ForbiddenCallTimePassByReferenceSniff.php 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. <?php
  2. /**
  3. * PHPCompatibility, an external standard for PHP_CodeSniffer.
  4. *
  5. * @package PHPCompatibility
  6. * @copyright 2009-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\Syntax;
  11. use PHPCompatibility\Sniff;
  12. use PHP_CodeSniffer_File as File;
  13. use PHP_CodeSniffer_Tokens as Tokens;
  14. /**
  15. * Detect the use of call time pass by reference.
  16. *
  17. * This behaviour has been deprecated in PHP 5.3 and removed in PHP 5.4.
  18. *
  19. * PHP version 5.4
  20. *
  21. * @link https://wiki.php.net/rfc/calltimebyref
  22. * @link https://www.php.net/manual/en/language.references.pass.php
  23. *
  24. * @since 5.5
  25. * @since 7.0.8 This sniff now throws a warning (deprecated) or an error (removed) depending
  26. * on the `testVersion` set. Previously it would always throw an error.
  27. */
  28. class ForbiddenCallTimePassByReferenceSniff extends Sniff
  29. {
  30. /**
  31. * Tokens that represent assignments or equality comparisons.
  32. *
  33. * Near duplicate of Tokens::$assignmentTokens + Tokens::$equalityTokens.
  34. * Copied in for PHPCS cross-version compatibility.
  35. *
  36. * @since 8.1.0
  37. *
  38. * @var array
  39. */
  40. private $assignOrCompare = array(
  41. // Equality tokens.
  42. 'T_IS_EQUAL' => true,
  43. 'T_IS_NOT_EQUAL' => true,
  44. 'T_IS_IDENTICAL' => true,
  45. 'T_IS_NOT_IDENTICAL' => true,
  46. 'T_IS_SMALLER_OR_EQUAL' => true,
  47. 'T_IS_GREATER_OR_EQUAL' => true,
  48. // Assignment tokens.
  49. 'T_EQUAL' => true,
  50. 'T_AND_EQUAL' => true,
  51. 'T_OR_EQUAL' => true,
  52. 'T_CONCAT_EQUAL' => true,
  53. 'T_DIV_EQUAL' => true,
  54. 'T_MINUS_EQUAL' => true,
  55. 'T_POW_EQUAL' => true,
  56. 'T_MOD_EQUAL' => true,
  57. 'T_MUL_EQUAL' => true,
  58. 'T_PLUS_EQUAL' => true,
  59. 'T_XOR_EQUAL' => true,
  60. 'T_DOUBLE_ARROW' => true,
  61. 'T_SL_EQUAL' => true,
  62. 'T_SR_EQUAL' => true,
  63. 'T_COALESCE_EQUAL' => true,
  64. 'T_ZSR_EQUAL' => true,
  65. );
  66. /**
  67. * Returns an array of tokens this test wants to listen for.
  68. *
  69. * @since 5.5
  70. *
  71. * @return array
  72. */
  73. public function register()
  74. {
  75. return array(
  76. \T_STRING,
  77. \T_VARIABLE,
  78. );
  79. }
  80. /**
  81. * Processes this test, when one of its tokens is encountered.
  82. *
  83. * @since 5.5
  84. *
  85. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  86. * @param int $stackPtr The position of the current token
  87. * in the stack passed in $tokens.
  88. *
  89. * @return void
  90. */
  91. public function process(File $phpcsFile, $stackPtr)
  92. {
  93. if ($this->supportsAbove('5.3') === false) {
  94. return;
  95. }
  96. $tokens = $phpcsFile->getTokens();
  97. // Skip tokens that are the names of functions or classes
  98. // within their definitions. For example: function myFunction...
  99. // "myFunction" is T_STRING but we should skip because it is not a
  100. // function or method *call*.
  101. $findTokens = Tokens::$emptyTokens;
  102. $findTokens[] = \T_BITWISE_AND;
  103. $prevNonEmpty = $phpcsFile->findPrevious(
  104. $findTokens,
  105. ($stackPtr - 1),
  106. null,
  107. true
  108. );
  109. if ($prevNonEmpty !== false && \in_array($tokens[$prevNonEmpty]['type'], array('T_FUNCTION', 'T_CLASS', 'T_INTERFACE', 'T_TRAIT'), true)) {
  110. return;
  111. }
  112. // If the next non-whitespace token after the function or method call
  113. // is not an opening parenthesis then it can't really be a *call*.
  114. $openBracket = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
  115. if ($openBracket === false || $tokens[$openBracket]['code'] !== \T_OPEN_PARENTHESIS
  116. || isset($tokens[$openBracket]['parenthesis_closer']) === false
  117. ) {
  118. return;
  119. }
  120. // Get the function call parameters.
  121. $parameters = $this->getFunctionCallParameters($phpcsFile, $stackPtr);
  122. if (\count($parameters) === 0) {
  123. return;
  124. }
  125. // Which nesting level is the one we are interested in ?
  126. $nestedParenthesisCount = 1;
  127. if (isset($tokens[$openBracket]['nested_parenthesis'])) {
  128. $nestedParenthesisCount = \count($tokens[$openBracket]['nested_parenthesis']) + 1;
  129. }
  130. foreach ($parameters as $parameter) {
  131. if ($this->isCallTimePassByReferenceParam($phpcsFile, $parameter, $nestedParenthesisCount) === true) {
  132. // T_BITWISE_AND represents a pass-by-reference.
  133. $error = 'Using a call-time pass-by-reference is deprecated since PHP 5.3';
  134. $isError = false;
  135. $errorCode = 'Deprecated';
  136. if ($this->supportsAbove('5.4')) {
  137. $error .= ' and prohibited since PHP 5.4';
  138. $isError = true;
  139. $errorCode = 'NotAllowed';
  140. }
  141. $this->addMessage($phpcsFile, $error, $parameter['start'], $isError, $errorCode);
  142. }
  143. }
  144. }
  145. /**
  146. * Determine whether a parameter is passed by reference.
  147. *
  148. * @since 7.0.6 Split off from the `process()` method.
  149. *
  150. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  151. * @param array $parameter Information on the current parameter
  152. * to be examined.
  153. * @param int $nestingLevel Target nesting level.
  154. *
  155. * @return bool
  156. */
  157. protected function isCallTimePassByReferenceParam(File $phpcsFile, $parameter, $nestingLevel)
  158. {
  159. $tokens = $phpcsFile->getTokens();
  160. $searchStartToken = $parameter['start'] - 1;
  161. $searchEndToken = $parameter['end'] + 1;
  162. $nextVariable = $searchStartToken;
  163. do {
  164. $nextVariable = $phpcsFile->findNext(array(\T_VARIABLE, \T_OPEN_SHORT_ARRAY, \T_CLOSURE), ($nextVariable + 1), $searchEndToken);
  165. if ($nextVariable === false) {
  166. return false;
  167. }
  168. // Ignore anything within short array definition brackets.
  169. if ($tokens[$nextVariable]['type'] === 'T_OPEN_SHORT_ARRAY'
  170. && (isset($tokens[$nextVariable]['bracket_opener'])
  171. && $tokens[$nextVariable]['bracket_opener'] === $nextVariable)
  172. && isset($tokens[$nextVariable]['bracket_closer'])
  173. ) {
  174. // Skip forward to the end of the short array definition.
  175. $nextVariable = $tokens[$nextVariable]['bracket_closer'];
  176. continue;
  177. }
  178. // Skip past closures passed as function parameters.
  179. if ($tokens[$nextVariable]['type'] === 'T_CLOSURE'
  180. && (isset($tokens[$nextVariable]['scope_condition'])
  181. && $tokens[$nextVariable]['scope_condition'] === $nextVariable)
  182. && isset($tokens[$nextVariable]['scope_closer'])
  183. ) {
  184. // Skip forward to the end of the closure declaration.
  185. $nextVariable = $tokens[$nextVariable]['scope_closer'];
  186. continue;
  187. }
  188. // Make sure the variable belongs directly to this function call
  189. // and is not inside a nested function call or array.
  190. if (isset($tokens[$nextVariable]['nested_parenthesis']) === false
  191. || (\count($tokens[$nextVariable]['nested_parenthesis']) !== $nestingLevel)
  192. ) {
  193. continue;
  194. }
  195. // Checking this: $value = my_function(...[*]$arg...).
  196. $tokenBefore = $phpcsFile->findPrevious(
  197. Tokens::$emptyTokens,
  198. ($nextVariable - 1),
  199. $searchStartToken,
  200. true
  201. );
  202. if ($tokenBefore === false || $tokens[$tokenBefore]['code'] !== \T_BITWISE_AND) {
  203. // Nothing before the token or no &.
  204. continue;
  205. }
  206. if ($phpcsFile->isReference($tokenBefore) === false) {
  207. continue;
  208. }
  209. // Checking this: $value = my_function(...[*]&$arg...).
  210. $tokenBefore = $phpcsFile->findPrevious(
  211. Tokens::$emptyTokens,
  212. ($tokenBefore - 1),
  213. $searchStartToken,
  214. true
  215. );
  216. // Prevent false positive on assign by reference and compare with reference
  217. // within function call parameters.
  218. if (isset($this->assignOrCompare[$tokens[$tokenBefore]['type']])) {
  219. continue;
  220. }
  221. // The found T_BITWISE_AND represents a pass-by-reference.
  222. return true;
  223. } while ($nextVariable < $searchEndToken);
  224. // This code should never be reached, but here in case of weird bugs.
  225. return false;
  226. }
  227. }