ValidIntegersSniff.php 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  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\Miscellaneous;
  11. use PHPCompatibility\Sniff;
  12. use PHP_CodeSniffer_File as File;
  13. /**
  14. * Check for valid integer types and values.
  15. *
  16. * Checks:
  17. * - PHP 5.4 introduced binary integers.
  18. * - PHP 7.0 removed tolerance for invalid octals. These were truncated prior to PHP 7
  19. * and give a parse error since PHP 7.
  20. * - PHP 7.0 removed support for recognizing hexadecimal numeric strings as numeric.
  21. * Type juggling and recognition was inconsistent prior to PHP 7. As of PHP 7, they
  22. * are no longer treated as numeric.
  23. *
  24. * PHP version 5.4+
  25. *
  26. * @link https://wiki.php.net/rfc/binnotation4ints
  27. * @link https://wiki.php.net/rfc/remove_hex_support_in_numeric_strings
  28. * @link https://www.php.net/manual/en/language.types.integer.php
  29. *
  30. * @since 7.0.3
  31. * @since 7.0.8 This sniff now throws a warning instead of an error for invalid binary integers.
  32. */
  33. class ValidIntegersSniff extends Sniff
  34. {
  35. /**
  36. * Whether PHPCS is run on a PHP < 5.4.
  37. *
  38. * @since 7.0.3
  39. *
  40. * @var bool
  41. */
  42. protected $isLowPHPVersion = false;
  43. /**
  44. * Returns an array of tokens this test wants to listen for.
  45. *
  46. * @since 7.0.3
  47. *
  48. * @return array
  49. */
  50. public function register()
  51. {
  52. $this->isLowPHPVersion = version_compare(\PHP_VERSION_ID, '50400', '<');
  53. return array(
  54. \T_LNUMBER, // Binary, octal integers.
  55. \T_CONSTANT_ENCAPSED_STRING, // Hex numeric string.
  56. );
  57. }
  58. /**
  59. * Processes this test, when one of its tokens is encountered.
  60. *
  61. * @since 7.0.3
  62. *
  63. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  64. * @param int $stackPtr The position of the current token in
  65. * the stack.
  66. *
  67. * @return void
  68. */
  69. public function process(File $phpcsFile, $stackPtr)
  70. {
  71. $tokens = $phpcsFile->getTokens();
  72. $token = $tokens[$stackPtr];
  73. if ($this->couldBeBinaryInteger($tokens, $stackPtr) === true) {
  74. if ($this->supportsBelow('5.3')) {
  75. $error = 'Binary integer literals were not present in PHP version 5.3 or earlier. Found: %s';
  76. if ($this->isLowPHPVersion === false) {
  77. $data = array($token['content']);
  78. } else {
  79. $data = array($this->getBinaryInteger($phpcsFile, $tokens, $stackPtr));
  80. }
  81. $phpcsFile->addError($error, $stackPtr, 'BinaryIntegerFound', $data);
  82. }
  83. if ($this->isInvalidBinaryInteger($tokens, $stackPtr) === true) {
  84. $error = 'Invalid binary integer detected. Found: %s';
  85. $data = array($this->getBinaryInteger($phpcsFile, $tokens, $stackPtr));
  86. $phpcsFile->addWarning($error, $stackPtr, 'InvalidBinaryIntegerFound', $data);
  87. }
  88. return;
  89. }
  90. $isError = $this->supportsAbove('7.0');
  91. $data = array( $token['content'] );
  92. if ($this->isInvalidOctalInteger($tokens, $stackPtr) === true) {
  93. $this->addMessage(
  94. $phpcsFile,
  95. 'Invalid octal integer detected. Prior to PHP 7 this would lead to a truncated number. From PHP 7 onwards this causes a parse error. Found: %s',
  96. $stackPtr,
  97. $isError,
  98. 'InvalidOctalIntegerFound',
  99. $data
  100. );
  101. return;
  102. }
  103. if ($this->isHexidecimalNumericString($tokens, $stackPtr) === true) {
  104. $this->addMessage(
  105. $phpcsFile,
  106. 'The behaviour of hexadecimal numeric strings was inconsistent prior to PHP 7 and support has been removed in PHP 7. Found: %s',
  107. $stackPtr,
  108. $isError,
  109. 'HexNumericStringFound',
  110. $data
  111. );
  112. return;
  113. }
  114. }
  115. /**
  116. * Could the current token potentially be a binary integer ?
  117. *
  118. * @since 7.0.3
  119. *
  120. * @param array $tokens Token stack.
  121. * @param int $stackPtr The current position in the token stack.
  122. *
  123. * @return bool
  124. */
  125. private function couldBeBinaryInteger($tokens, $stackPtr)
  126. {
  127. $token = $tokens[$stackPtr];
  128. if ($token['code'] !== \T_LNUMBER) {
  129. return false;
  130. }
  131. if ($this->isLowPHPVersion === false) {
  132. return (preg_match('`^0b[0-1]+$`iD', $token['content']) === 1);
  133. }
  134. // Pre-5.4, binary strings are tokenized as T_LNUMBER (0) + T_STRING ("b01010101").
  135. // At this point, we don't yet care whether it's a valid binary int, that's a separate check.
  136. else {
  137. return($token['content'] === '0' && $tokens[$stackPtr + 1]['code'] === \T_STRING && preg_match('`^b[0-9]+$`iD', $tokens[$stackPtr + 1]['content']) === 1);
  138. }
  139. }
  140. /**
  141. * Is the current token an invalid binary integer ?
  142. *
  143. * @since 7.0.3
  144. *
  145. * @param array $tokens Token stack.
  146. * @param int $stackPtr The current position in the token stack.
  147. *
  148. * @return bool
  149. */
  150. private function isInvalidBinaryInteger($tokens, $stackPtr)
  151. {
  152. if ($this->couldBeBinaryInteger($tokens, $stackPtr) === false) {
  153. return false;
  154. }
  155. if ($this->isLowPHPVersion === false) {
  156. // If it's an invalid binary int, the token will be split into two T_LNUMBER tokens.
  157. return ($tokens[$stackPtr + 1]['code'] === \T_LNUMBER);
  158. } else {
  159. return (preg_match('`^b[0-1]+$`iD', $tokens[$stackPtr + 1]['content']) === 0);
  160. }
  161. }
  162. /**
  163. * Retrieve the content of the tokens which together look like a binary integer.
  164. *
  165. * @since 7.0.3
  166. *
  167. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  168. * @param array $tokens Token stack.
  169. * @param int $stackPtr The position of the current token in
  170. * the stack.
  171. *
  172. * @return string
  173. */
  174. private function getBinaryInteger(File $phpcsFile, $tokens, $stackPtr)
  175. {
  176. $length = 2; // PHP < 5.4 T_LNUMBER + T_STRING.
  177. if ($this->isLowPHPVersion === false) {
  178. $i = $stackPtr;
  179. while ($tokens[$i]['code'] === \T_LNUMBER) {
  180. $i++;
  181. }
  182. $length = ($i - $stackPtr);
  183. }
  184. return $phpcsFile->getTokensAsString($stackPtr, $length);
  185. }
  186. /**
  187. * Is the current token an invalid octal integer ?
  188. *
  189. * @since 7.0.3
  190. *
  191. * @param array $tokens Token stack.
  192. * @param int $stackPtr The current position in the token stack.
  193. *
  194. * @return bool
  195. */
  196. private function isInvalidOctalInteger($tokens, $stackPtr)
  197. {
  198. $token = $tokens[$stackPtr];
  199. if ($token['code'] === \T_LNUMBER && preg_match('`^0[0-7]*[8-9]+[0-9]*$`D', $token['content']) === 1) {
  200. return true;
  201. }
  202. return false;
  203. }
  204. /**
  205. * Is the current token a hexidecimal numeric string ?
  206. *
  207. * @since 7.0.3
  208. *
  209. * @param array $tokens Token stack.
  210. * @param int $stackPtr The current position in the token stack.
  211. *
  212. * @return bool
  213. */
  214. private function isHexidecimalNumericString($tokens, $stackPtr)
  215. {
  216. $token = $tokens[$stackPtr];
  217. if ($token['code'] === \T_CONSTANT_ENCAPSED_STRING && preg_match('`^0x[a-f0-9]+$`iD', $this->stripQuotes($token['content'])) === 1) {
  218. return true;
  219. }
  220. return false;
  221. }
  222. }