NewFunctionArrayDereferencingSniff.php 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  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\Syntax;
  11. use PHPCompatibility\Sniff;
  12. use PHP_CodeSniffer_File as File;
  13. use PHP_CodeSniffer_Tokens as Tokens;
  14. /**
  15. * Detect function array dereferencing as introduced in PHP 5.4.
  16. *
  17. * PHP 5.4 supports direct array dereferencing on the return of a method/function call.
  18. *
  19. * As of PHP 7.0, this also works when using curly braces for the dereferencing.
  20. * While unclear, this most likely has to do with the Uniform Variable Syntax changes.
  21. *
  22. * PHP version 5.4
  23. * PHP version 7.0
  24. *
  25. * @link https://www.php.net/manual/en/language.types.array.php#example-63
  26. * @link https://www.php.net/manual/en/migration54.new-features.php
  27. * @link https://wiki.php.net/rfc/functionarraydereferencing
  28. * @link https://wiki.php.net/rfc/uniform_variable_syntax
  29. *
  30. * {@internal The reason for splitting the logic of this sniff into different methods is
  31. * to allow re-use of the logic by the PHP 7.4 RemovedCurlyBraceArrayAccess sniff.}
  32. *
  33. * @since 7.0.0
  34. * @since 9.3.0 Now also detects dereferencing using curly braces.
  35. */
  36. class NewFunctionArrayDereferencingSniff extends Sniff
  37. {
  38. /**
  39. * Returns an array of tokens this test wants to listen for.
  40. *
  41. * @since 7.0.0
  42. *
  43. * @return array
  44. */
  45. public function register()
  46. {
  47. return array(\T_STRING);
  48. }
  49. /**
  50. * Processes this test, when one of its tokens is encountered.
  51. *
  52. * @since 7.0.0
  53. *
  54. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  55. * @param int $stackPtr The position of the current token in
  56. * the stack passed in $tokens.
  57. *
  58. * @return void
  59. */
  60. public function process(File $phpcsFile, $stackPtr)
  61. {
  62. if ($this->supportsBelow('5.6') === false) {
  63. return;
  64. }
  65. $dereferencing = $this->isFunctionArrayDereferencing($phpcsFile, $stackPtr);
  66. if (empty($dereferencing)) {
  67. return;
  68. }
  69. $tokens = $phpcsFile->getTokens();
  70. $supports53 = $this->supportsBelow('5.3');
  71. foreach ($dereferencing as $openBrace => $closeBrace) {
  72. if ($supports53 === true
  73. && $tokens[$openBrace]['type'] === 'T_OPEN_SQUARE_BRACKET'
  74. ) {
  75. $phpcsFile->addError(
  76. 'Function array dereferencing is not present in PHP version 5.3 or earlier',
  77. $openBrace,
  78. 'Found'
  79. );
  80. continue;
  81. }
  82. // PHP 7.0 function array dereferencing using curly braces.
  83. if ($tokens[$openBrace]['type'] === 'T_OPEN_CURLY_BRACKET') {
  84. $phpcsFile->addError(
  85. 'Function array dereferencing using curly braces is not present in PHP version 5.6 or earlier',
  86. $openBrace,
  87. 'FoundUsingCurlies'
  88. );
  89. }
  90. }
  91. }
  92. /**
  93. * Check if the return of a function/method call is being dereferenced.
  94. *
  95. * @since 9.3.0 Logic split off from the process method.
  96. *
  97. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  98. * @param int $stackPtr The position of the current token in
  99. * the stack passed in $tokens.
  100. *
  101. * @return array Array containing stack pointers to the open/close braces
  102. * involved in the function dereferencing;
  103. * or an empty array if no function dereferencing was detected.
  104. */
  105. public function isFunctionArrayDereferencing(File $phpcsFile, $stackPtr)
  106. {
  107. $tokens = $phpcsFile->getTokens();
  108. // Next non-empty token should be the open parenthesis.
  109. $openParenthesis = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true, null, true);
  110. if ($openParenthesis === false || $tokens[$openParenthesis]['code'] !== \T_OPEN_PARENTHESIS) {
  111. return array();
  112. }
  113. // Don't throw errors during live coding.
  114. if (isset($tokens[$openParenthesis]['parenthesis_closer']) === false) {
  115. return array();
  116. }
  117. // Is this T_STRING really a function or method call ?
  118. $prevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
  119. if ($prevToken !== false
  120. && \in_array($tokens[$prevToken]['code'], array(\T_DOUBLE_COLON, \T_OBJECT_OPERATOR), true) === false
  121. ) {
  122. if ($tokens[$prevToken]['code'] === \T_BITWISE_AND) {
  123. // This may be a function declared by reference.
  124. $prevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevToken - 1), null, true);
  125. }
  126. $ignore = array(
  127. \T_FUNCTION => true,
  128. \T_CONST => true,
  129. \T_USE => true,
  130. \T_NEW => true,
  131. \T_CLASS => true,
  132. \T_INTERFACE => true,
  133. );
  134. if (isset($ignore[$tokens[$prevToken]['code']]) === true) {
  135. // Not a call to a PHP function or method.
  136. return array();
  137. }
  138. }
  139. $current = $tokens[$openParenthesis]['parenthesis_closer'];
  140. $braces = array();
  141. do {
  142. $nextNonEmpty = $phpcsFile->findNext(Tokens::$emptyTokens, ($current + 1), null, true, null, true);
  143. if ($nextNonEmpty === false) {
  144. break;
  145. }
  146. if ($tokens[$nextNonEmpty]['type'] === 'T_OPEN_SQUARE_BRACKET'
  147. || $tokens[$nextNonEmpty]['type'] === 'T_OPEN_CURLY_BRACKET' // PHP 7.0+.
  148. ) {
  149. if (isset($tokens[$nextNonEmpty]['bracket_closer']) === false) {
  150. // Live coding or parse error.
  151. break;
  152. }
  153. $braces[$nextNonEmpty] = $tokens[$nextNonEmpty]['bracket_closer'];
  154. // Continue, just in case there is nested array access, i.e. `echo $foo->bar()[0][2];`.
  155. $current = $tokens[$nextNonEmpty]['bracket_closer'];
  156. continue;
  157. }
  158. // If we're still here, we've reached the end of the function call.
  159. break;
  160. } while (true);
  161. return $braces;
  162. }
  163. }