NewInterfacesSniff.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  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\Interfaces;
  11. use PHPCompatibility\AbstractNewFeatureSniff;
  12. use PHPCompatibility\PHPCSHelper;
  13. use PHP_CodeSniffer_File as File;
  14. /**
  15. * Detect use of new PHP native interfaces and unsupported interface methods.
  16. *
  17. * PHP version 5.0+
  18. *
  19. * @since 7.0.3
  20. * @since 7.1.0 Now extends the `AbstractNewFeatureSniff` instead of the base `Sniff` class..
  21. * @since 7.1.4 Now also detects new interfaces when used as parameter type declarations.
  22. * @since 8.2.0 Now also detects new interfaces when used as return type declarations.
  23. */
  24. class NewInterfacesSniff extends AbstractNewFeatureSniff
  25. {
  26. /**
  27. * A list of new interfaces, not present in older versions.
  28. *
  29. * The array lists : version number with false (not present) or true (present).
  30. * If's sufficient to list the first version where the interface appears.
  31. *
  32. * @since 7.0.3
  33. *
  34. * @var array(string => array(string => bool))
  35. */
  36. protected $newInterfaces = array(
  37. 'Traversable' => array(
  38. '4.4' => false,
  39. '5.0' => true,
  40. ),
  41. 'Reflector' => array(
  42. '4.4' => false,
  43. '5.0' => true,
  44. ),
  45. 'Countable' => array(
  46. '5.0' => false,
  47. '5.1' => true,
  48. ),
  49. 'OuterIterator' => array(
  50. '5.0' => false,
  51. '5.1' => true,
  52. ),
  53. 'RecursiveIterator' => array(
  54. '5.0' => false,
  55. '5.1' => true,
  56. ),
  57. 'SeekableIterator' => array(
  58. '5.0' => false,
  59. '5.1' => true,
  60. ),
  61. 'Serializable' => array(
  62. '5.0' => false,
  63. '5.1' => true,
  64. ),
  65. 'SplObserver' => array(
  66. '5.0' => false,
  67. '5.1' => true,
  68. ),
  69. 'SplSubject' => array(
  70. '5.0' => false,
  71. '5.1' => true,
  72. ),
  73. 'JsonSerializable' => array(
  74. '5.3' => false,
  75. '5.4' => true,
  76. ),
  77. 'SessionHandlerInterface' => array(
  78. '5.3' => false,
  79. '5.4' => true,
  80. ),
  81. 'DateTimeInterface' => array(
  82. '5.4' => false,
  83. '5.5' => true,
  84. ),
  85. 'SessionIdInterface' => array(
  86. '5.5.0' => false,
  87. '5.5.1' => true,
  88. ),
  89. 'Throwable' => array(
  90. '5.6' => false,
  91. '7.0' => true,
  92. ),
  93. 'SessionUpdateTimestampHandlerInterface' => array(
  94. '5.6' => false,
  95. '7.0' => true,
  96. ),
  97. );
  98. /**
  99. * A list of methods which cannot be used in combination with particular interfaces.
  100. *
  101. * @since 7.0.3
  102. *
  103. * @var array(string => array(string => string))
  104. */
  105. protected $unsupportedMethods = array(
  106. 'Serializable' => array(
  107. '__sleep' => 'https://www.php.net/serializable',
  108. '__wakeup' => 'https://www.php.net/serializable',
  109. ),
  110. );
  111. /**
  112. * Returns an array of tokens this test wants to listen for.
  113. *
  114. * @since 7.0.3
  115. *
  116. * @return array
  117. */
  118. public function register()
  119. {
  120. // Handle case-insensitivity of interface names.
  121. $this->newInterfaces = $this->arrayKeysToLowercase($this->newInterfaces);
  122. $this->unsupportedMethods = $this->arrayKeysToLowercase($this->unsupportedMethods);
  123. $targets = array(
  124. \T_CLASS,
  125. \T_FUNCTION,
  126. \T_CLOSURE,
  127. );
  128. if (\defined('T_ANON_CLASS')) {
  129. $targets[] = \T_ANON_CLASS;
  130. }
  131. if (\defined('T_RETURN_TYPE')) {
  132. $targets[] = \T_RETURN_TYPE;
  133. }
  134. return $targets;
  135. }
  136. /**
  137. * Processes this test, when one of its tokens is encountered.
  138. *
  139. * @since 7.0.3
  140. *
  141. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  142. * @param int $stackPtr The position of the current token in
  143. * the stack passed in $tokens.
  144. *
  145. * @return void
  146. */
  147. public function process(File $phpcsFile, $stackPtr)
  148. {
  149. $tokens = $phpcsFile->getTokens();
  150. switch ($tokens[$stackPtr]['type']) {
  151. case 'T_CLASS':
  152. case 'T_ANON_CLASS':
  153. $this->processClassToken($phpcsFile, $stackPtr);
  154. break;
  155. case 'T_FUNCTION':
  156. case 'T_CLOSURE':
  157. $this->processFunctionToken($phpcsFile, $stackPtr);
  158. // Deal with older PHPCS versions which don't recognize return type hints
  159. // as well as newer PHPCS versions (3.3.0+) where the tokenization has changed.
  160. $returnTypeHint = $this->getReturnTypeHintToken($phpcsFile, $stackPtr);
  161. if ($returnTypeHint !== false) {
  162. $this->processReturnTypeToken($phpcsFile, $returnTypeHint);
  163. }
  164. break;
  165. case 'T_RETURN_TYPE':
  166. $this->processReturnTypeToken($phpcsFile, $stackPtr);
  167. break;
  168. default:
  169. // Deliberately left empty.
  170. break;
  171. }
  172. }
  173. /**
  174. * Processes this test for when a class token is encountered.
  175. *
  176. * - Detect classes implementing the new interfaces.
  177. * - Detect classes implementing the new interfaces with unsupported functions.
  178. *
  179. * @since 7.1.4 Split off from the `process()` method.
  180. *
  181. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  182. * @param int $stackPtr The position of the current token in
  183. * the stack passed in $tokens.
  184. *
  185. * @return void
  186. */
  187. private function processClassToken(File $phpcsFile, $stackPtr)
  188. {
  189. $interfaces = PHPCSHelper::findImplementedInterfaceNames($phpcsFile, $stackPtr);
  190. if (\is_array($interfaces) === false || $interfaces === array()) {
  191. return;
  192. }
  193. $tokens = $phpcsFile->getTokens();
  194. $checkMethods = false;
  195. if (isset($tokens[$stackPtr]['scope_closer'])) {
  196. $checkMethods = true;
  197. $scopeCloser = $tokens[$stackPtr]['scope_closer'];
  198. }
  199. foreach ($interfaces as $interface) {
  200. $interface = ltrim($interface, '\\');
  201. $interfaceLc = strtolower($interface);
  202. if (isset($this->newInterfaces[$interfaceLc]) === true) {
  203. $itemInfo = array(
  204. 'name' => $interface,
  205. 'nameLc' => $interfaceLc,
  206. );
  207. $this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
  208. }
  209. if ($checkMethods === true && isset($this->unsupportedMethods[$interfaceLc]) === true) {
  210. $nextFunc = $stackPtr;
  211. while (($nextFunc = $phpcsFile->findNext(\T_FUNCTION, ($nextFunc + 1), $scopeCloser)) !== false) {
  212. $funcName = $phpcsFile->getDeclarationName($nextFunc);
  213. $funcNameLc = strtolower($funcName);
  214. if ($funcNameLc === '') {
  215. continue;
  216. }
  217. if (isset($this->unsupportedMethods[$interfaceLc][$funcNameLc]) === true) {
  218. $error = 'Classes that implement interface %s do not support the method %s(). See %s';
  219. $errorCode = $this->stringToErrorCode($interface) . 'UnsupportedMethod';
  220. $data = array(
  221. $interface,
  222. $funcName,
  223. $this->unsupportedMethods[$interfaceLc][$funcNameLc],
  224. );
  225. $phpcsFile->addError($error, $nextFunc, $errorCode, $data);
  226. }
  227. }
  228. }
  229. }
  230. }
  231. /**
  232. * Processes this test for when a function token is encountered.
  233. *
  234. * - Detect new interfaces when used as a type hint.
  235. *
  236. * @since 7.1.4
  237. *
  238. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  239. * @param int $stackPtr The position of the current token in
  240. * the stack passed in $tokens.
  241. *
  242. * @return void
  243. */
  244. private function processFunctionToken(File $phpcsFile, $stackPtr)
  245. {
  246. $typeHints = $this->getTypeHintsFromFunctionDeclaration($phpcsFile, $stackPtr);
  247. if (empty($typeHints) || \is_array($typeHints) === false) {
  248. return;
  249. }
  250. foreach ($typeHints as $hint) {
  251. $typeHintLc = strtolower($hint);
  252. if (isset($this->newInterfaces[$typeHintLc]) === true) {
  253. $itemInfo = array(
  254. 'name' => $hint,
  255. 'nameLc' => $typeHintLc,
  256. );
  257. $this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
  258. }
  259. }
  260. }
  261. /**
  262. * Processes this test for when a return type token is encountered.
  263. *
  264. * - Detect new interfaces when used as a return type declaration.
  265. *
  266. * @since 8.2.0
  267. *
  268. * @param \PHP_CodeSniffer_File $phpcsFile The file being scanned.
  269. * @param int $stackPtr The position of the current token in
  270. * the stack passed in $tokens.
  271. *
  272. * @return void
  273. */
  274. private function processReturnTypeToken(File $phpcsFile, $stackPtr)
  275. {
  276. $returnTypeHint = $this->getReturnTypeHintName($phpcsFile, $stackPtr);
  277. if (empty($returnTypeHint)) {
  278. return;
  279. }
  280. $returnTypeHint = ltrim($returnTypeHint, '\\');
  281. $returnTypeHintLc = strtolower($returnTypeHint);
  282. if (isset($this->newInterfaces[$returnTypeHintLc]) === false) {
  283. return;
  284. }
  285. // Still here ? Then this is a return type declaration using a new interface.
  286. $itemInfo = array(
  287. 'name' => $returnTypeHint,
  288. 'nameLc' => $returnTypeHintLc,
  289. );
  290. $this->handleFeature($phpcsFile, $stackPtr, $itemInfo);
  291. }
  292. /**
  293. * Get the relevant sub-array for a specific item from a multi-dimensional array.
  294. *
  295. * @since 7.1.0
  296. *
  297. * @param array $itemInfo Base information about the item.
  298. *
  299. * @return array Version and other information about the item.
  300. */
  301. public function getItemArray(array $itemInfo)
  302. {
  303. return $this->newInterfaces[$itemInfo['nameLc']];
  304. }
  305. /**
  306. * Get the error message template for this sniff.
  307. *
  308. * @since 7.1.0
  309. *
  310. * @return string
  311. */
  312. protected function getErrorMsgTemplate()
  313. {
  314. return 'The built-in interface ' . parent::getErrorMsgTemplate();
  315. }
  316. }