OrderedClassElementsFixer.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. <?php
  2. /*
  3. * This file is part of PHP CS Fixer.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  7. *
  8. * This source file is subject to the MIT license that is bundled
  9. * with this source code in the file LICENSE.
  10. */
  11. namespace PhpCsFixer\Fixer\ClassNotation;
  12. use PhpCsFixer\AbstractFixer;
  13. use PhpCsFixer\ConfigurationException\InvalidFixerConfigurationException;
  14. use PhpCsFixer\Tokenizer\Token;
  15. use PhpCsFixer\Tokenizer\Tokens;
  16. /**
  17. * @author Gregor Harlan <gharlan@web.de>
  18. */
  19. final class OrderedClassElementsFixer extends AbstractFixer
  20. {
  21. /**
  22. * @var array Array containing all class element base types (keys) and their parent types (values)
  23. */
  24. private static $typeHierarchy = array(
  25. 'use_trait' => null,
  26. 'public' => null,
  27. 'protected' => null,
  28. 'private' => null,
  29. 'constant' => null,
  30. 'constant_public' => array('constant', 'public'),
  31. 'constant_protected' => array('constant', 'protected'),
  32. 'constant_private' => array('constant', 'private'),
  33. 'property' => null,
  34. 'property_static' => array('property'),
  35. 'property_public' => array('property', 'public'),
  36. 'property_protected' => array('property', 'protected'),
  37. 'property_private' => array('property', 'private'),
  38. 'property_public_static' => array('property_static', 'property_public'),
  39. 'property_protected_static' => array('property_static', 'property_protected'),
  40. 'property_private_static' => array('property_static', 'property_private'),
  41. 'method' => null,
  42. 'method_static' => array('method'),
  43. 'method_public' => array('method', 'public'),
  44. 'method_protected' => array('method', 'protected'),
  45. 'method_private' => array('method', 'private'),
  46. 'method_public_static' => array('method_static', 'method_public'),
  47. 'method_protected_static' => array('method_static', 'method_protected'),
  48. 'method_private_static' => array('method_static', 'method_private'),
  49. );
  50. /**
  51. * @var array Array containing special method types
  52. */
  53. private static $specialTypes = array(
  54. 'construct' => null,
  55. 'destruct' => null,
  56. 'magic' => null,
  57. 'phpunit' => null,
  58. );
  59. /**
  60. * @var string[] Default order/configuration
  61. */
  62. private static $defaultOrder = array(
  63. 'use_trait',
  64. 'constant_public',
  65. 'constant_protected',
  66. 'constant_private',
  67. 'property_public',
  68. 'property_protected',
  69. 'property_private',
  70. 'construct',
  71. 'destruct',
  72. 'magic',
  73. 'phpunit',
  74. 'method_public',
  75. 'method_protected',
  76. 'method_private',
  77. );
  78. /**
  79. * @var array Resolved configuration array (type => position)
  80. */
  81. private $typePosition;
  82. /**
  83. * {@inheritdoc}
  84. */
  85. public function configure(array $configuration = null)
  86. {
  87. if (null === $configuration) {
  88. $configuration = self::$defaultOrder;
  89. }
  90. $this->typePosition = array();
  91. $pos = 0;
  92. foreach ($configuration as $type) {
  93. if (!array_key_exists($type, self::$typeHierarchy) && !array_key_exists($type, self::$specialTypes)) {
  94. throw new InvalidFixerConfigurationException($this->getName(), sprintf('Unknown class element type "%s".', $type));
  95. }
  96. $this->typePosition[$type] = $pos++;
  97. }
  98. foreach (self::$typeHierarchy as $type => $parents) {
  99. if (isset($this->typePosition[$type])) {
  100. continue;
  101. }
  102. if (!$parents) {
  103. $this->typePosition[$type] = null;
  104. continue;
  105. }
  106. foreach ($parents as $parent) {
  107. if (isset($this->typePosition[$parent])) {
  108. $this->typePosition[$type] = $this->typePosition[$parent];
  109. continue 2;
  110. }
  111. }
  112. $this->typePosition[$type] = null;
  113. }
  114. $lastPosition = count($configuration);
  115. foreach ($this->typePosition as &$pos) {
  116. if (null === $pos) {
  117. $pos = $lastPosition;
  118. }
  119. // last digit is used by phpunit method ordering
  120. $pos *= 10;
  121. }
  122. }
  123. /**
  124. * {@inheritdoc}
  125. */
  126. public function isCandidate(Tokens $tokens)
  127. {
  128. return $tokens->isAnyTokenKindsFound(Token::getClassyTokenKinds());
  129. }
  130. /**
  131. * {@inheritdoc}
  132. */
  133. public function fix(\SplFileInfo $file, Tokens $tokens)
  134. {
  135. for ($i = 1, $count = $tokens->count(); $i < $count; ++$i) {
  136. if (!$tokens[$i]->isClassy()) {
  137. continue;
  138. }
  139. $i = $tokens->getNextTokenOfKind($i, array('{'));
  140. $elements = $this->getElements($tokens, $i);
  141. if (!$elements) {
  142. continue;
  143. }
  144. $sorted = $this->sortElements($elements);
  145. $endIndex = $elements[count($elements) - 1]['end'];
  146. if ($sorted !== $elements) {
  147. $this->sortTokens($tokens, $i, $endIndex, $sorted);
  148. }
  149. $i = $endIndex;
  150. }
  151. }
  152. /**
  153. * {@inheritdoc}
  154. */
  155. public function getDescription()
  156. {
  157. return 'Orders the elements of classes/interfaces/traits.';
  158. }
  159. /**
  160. * {@inheritdoc}
  161. */
  162. public function getPriority()
  163. {
  164. // must run before MethodSeparationFixer, NoBlankLinesAfterClassOpeningFixer and SpaceAfterSemicolonFixer.
  165. // must run after ProtectedToPrivateFixer.
  166. return 65;
  167. }
  168. /**
  169. * @param Tokens $tokens
  170. * @param int $startIndex
  171. *
  172. * @return array[]
  173. */
  174. private function getElements(Tokens $tokens, $startIndex)
  175. {
  176. static $elementTokenKinds = array(CT_USE_TRAIT, T_CONST, T_VARIABLE, T_FUNCTION);
  177. ++$startIndex;
  178. $elements = array();
  179. while (true) {
  180. $element = array(
  181. 'start' => $startIndex,
  182. 'visibility' => 'public',
  183. 'static' => false,
  184. );
  185. for ($i = $startIndex; ; ++$i) {
  186. $token = $tokens[$i];
  187. // class end
  188. if ($token->equals('}')) {
  189. return $elements;
  190. }
  191. if ($token->isGivenKind(T_STATIC)) {
  192. $element['static'] = true;
  193. continue;
  194. }
  195. if ($token->isGivenKind(array(T_PROTECTED, T_PRIVATE))) {
  196. $element['visibility'] = strtolower($token->getContent());
  197. continue;
  198. }
  199. if (!$token->isGivenKind($elementTokenKinds)) {
  200. continue;
  201. }
  202. $type = $this->detectElementType($tokens, $i);
  203. if (is_array($type)) {
  204. $element['type'] = $type[0];
  205. $element['name'] = $type[1];
  206. } else {
  207. $element['type'] = $type;
  208. }
  209. $element['end'] = $this->findElementEnd($tokens, $i);
  210. break;
  211. }
  212. $elements[] = $element;
  213. $startIndex = $element['end'] + 1;
  214. }
  215. }
  216. /**
  217. * @param Tokens $tokens
  218. * @param int $index
  219. *
  220. * @return string|array type or array of type and name
  221. */
  222. private function detectElementType(Tokens $tokens, $index)
  223. {
  224. $token = $tokens[$index];
  225. if ($token->isGivenKind(CT_USE_TRAIT)) {
  226. return 'use_trait';
  227. }
  228. if ($token->isGivenKind(T_CONST)) {
  229. return 'constant';
  230. }
  231. if ($token->isGivenKind(T_VARIABLE)) {
  232. return 'property';
  233. }
  234. $nameToken = $tokens[$tokens->getNextMeaningfulToken($index)];
  235. if ($nameToken->equals(array(T_STRING, '__construct'), false)) {
  236. return 'construct';
  237. }
  238. if ($nameToken->equals(array(T_STRING, '__destruct'), false)) {
  239. return 'destruct';
  240. }
  241. if (
  242. $nameToken->equalsAny(array(
  243. array(T_STRING, 'setUpBeforeClass'),
  244. array(T_STRING, 'tearDownAfterClass'),
  245. array(T_STRING, 'setUp'),
  246. array(T_STRING, 'tearDown'),
  247. ), false)
  248. ) {
  249. return array('phpunit', strtolower($nameToken->getContent()));
  250. }
  251. if ('__' === substr($nameToken->getContent(), 0, 2)) {
  252. return 'magic';
  253. }
  254. return 'method';
  255. }
  256. /**
  257. * @param Tokens $tokens
  258. * @param int $index
  259. *
  260. * @return int
  261. */
  262. private function findElementEnd(Tokens $tokens, $index)
  263. {
  264. $index = $tokens->getNextTokenOfKind($index, array('{', ';'));
  265. if ($tokens[$index]->equals('{')) {
  266. $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
  267. }
  268. for (++$index; $tokens[$index]->isWhitespace(" \t") || $tokens[$index]->isComment(); ++$index);
  269. --$index;
  270. return $tokens[$index]->isWhitespace() ? $index - 1 : $index;
  271. }
  272. /**
  273. * @param array[] $elements
  274. *
  275. * @return array[]
  276. */
  277. private function sortElements(array $elements)
  278. {
  279. static $phpunitPositions = array(
  280. 'setupbeforeclass' => 1,
  281. 'teardownafterclass' => 2,
  282. 'setup' => 3,
  283. 'teardown' => 4,
  284. );
  285. foreach ($elements as &$element) {
  286. $type = $element['type'];
  287. if (array_key_exists($type, self::$specialTypes)) {
  288. if (isset($this->typePosition[$type])) {
  289. $element['position'] = $this->typePosition[$type];
  290. if ('phpunit' === $type) {
  291. $element['position'] += $phpunitPositions[$element['name']];
  292. }
  293. continue;
  294. }
  295. $type = 'method';
  296. }
  297. if (in_array($type, array('constant', 'property', 'method'), true)) {
  298. $type .= '_'.$element['visibility'];
  299. if ($element['static']) {
  300. $type .= '_static';
  301. }
  302. }
  303. $element['position'] = $this->typePosition[$type];
  304. }
  305. usort($elements, function (array $a, array $b) {
  306. if ($a['position'] === $b['position']) {
  307. // same group, preserve current order
  308. return $a['start'] > $b['start'] ? 1 : -1;
  309. }
  310. return $a['position'] > $b['position'] ? 1 : -1;
  311. });
  312. return $elements;
  313. }
  314. /**
  315. * @param Tokens $tokens
  316. * @param int $startIndex
  317. * @param int $endIndex
  318. * @param array[] $elements
  319. */
  320. private function sortTokens(Tokens $tokens, $startIndex, $endIndex, array $elements)
  321. {
  322. $replaceTokens = array();
  323. foreach ($elements as $element) {
  324. for ($i = $element['start']; $i <= $element['end']; ++$i) {
  325. $replaceTokens[] = clone $tokens[$i];
  326. }
  327. }
  328. $tokens->overrideRange($startIndex + 1, $endIndex, $replaceTokens);
  329. }
  330. }