ClassDefinitionFixer.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of PHP CS Fixer.
  5. *
  6. * (c) Fabien Potencier <fabien@symfony.com>
  7. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  8. *
  9. * This source file is subject to the MIT license that is bundled
  10. * with this source code in the file LICENSE.
  11. */
  12. namespace PhpCsFixer\Fixer\ClassNotation;
  13. use PhpCsFixer\AbstractFixer;
  14. use PhpCsFixer\Fixer\ConfigurableFixerInterface;
  15. use PhpCsFixer\Fixer\ConfigurableFixerTrait;
  16. use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
  17. use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
  18. use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
  19. use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
  20. use PhpCsFixer\FixerDefinition\CodeSample;
  21. use PhpCsFixer\FixerDefinition\FixerDefinition;
  22. use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
  23. use PhpCsFixer\Tokenizer\CT;
  24. use PhpCsFixer\Tokenizer\Token;
  25. use PhpCsFixer\Tokenizer\Tokens;
  26. use PhpCsFixer\Tokenizer\TokensAnalyzer;
  27. /**
  28. * Fixer for part of the rules defined in PSR2 ¶4.1 Extends and Implements and PSR12 ¶8. Anonymous Classes.
  29. *
  30. * @phpstan-type _ClassExtendsInfo array{start: int, numberOfExtends: int, multiLine: bool}
  31. * @phpstan-type _ClassImplementsInfo array{start: int, numberOfImplements: int, multiLine: bool}
  32. *
  33. * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
  34. *
  35. * @phpstan-type _AutogeneratedInputConfiguration array{
  36. * inline_constructor_arguments?: bool,
  37. * multi_line_extends_each_single_line?: bool,
  38. * single_item_single_line?: bool,
  39. * single_line?: bool,
  40. * space_before_parenthesis?: bool
  41. * }
  42. * @phpstan-type _AutogeneratedComputedConfiguration array{
  43. * inline_constructor_arguments: bool,
  44. * multi_line_extends_each_single_line: bool,
  45. * single_item_single_line: bool,
  46. * single_line: bool,
  47. * space_before_parenthesis: bool
  48. * }
  49. */
  50. final class ClassDefinitionFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
  51. {
  52. /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
  53. use ConfigurableFixerTrait;
  54. public function getDefinition(): FixerDefinitionInterface
  55. {
  56. return new FixerDefinition(
  57. 'Whitespace around the keywords of a class, trait, enum or interfaces definition should be one space.',
  58. [
  59. new CodeSample(
  60. '<?php
  61. class Foo extends Bar implements Baz, BarBaz
  62. {
  63. }
  64. final class Foo extends Bar implements Baz, BarBaz
  65. {
  66. }
  67. trait Foo
  68. {
  69. }
  70. $foo = new class extends Bar implements Baz, BarBaz {};
  71. '
  72. ),
  73. new CodeSample(
  74. '<?php
  75. class Foo
  76. extends Bar
  77. implements Baz, BarBaz
  78. {}
  79. ',
  80. ['single_line' => true]
  81. ),
  82. new CodeSample(
  83. '<?php
  84. class Foo
  85. extends Bar
  86. implements Baz
  87. {}
  88. ',
  89. ['single_item_single_line' => true]
  90. ),
  91. new CodeSample(
  92. '<?php
  93. interface Bar extends
  94. Bar, BarBaz, FooBarBaz
  95. {}
  96. ',
  97. ['multi_line_extends_each_single_line' => true]
  98. ),
  99. new CodeSample(
  100. '<?php
  101. $foo = new class(){};
  102. ',
  103. ['space_before_parenthesis' => true]
  104. ),
  105. new CodeSample(
  106. "<?php\n\$foo = new class(\n \$bar,\n \$baz\n) {};\n",
  107. ['inline_constructor_arguments' => true]
  108. ),
  109. ]
  110. );
  111. }
  112. /**
  113. * {@inheritdoc}
  114. *
  115. * Must run before BracesFixer, SingleLineEmptyBodyFixer.
  116. * Must run after NewWithBracesFixer, NewWithParenthesesFixer.
  117. */
  118. public function getPriority(): int
  119. {
  120. return 36;
  121. }
  122. public function isCandidate(Tokens $tokens): bool
  123. {
  124. return $tokens->isAnyTokenKindsFound(Token::getClassyTokenKinds());
  125. }
  126. protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
  127. {
  128. // -4, one for count to index, 3 because min. of tokens for a classy location.
  129. for ($index = $tokens->getSize() - 4; $index > 0; --$index) {
  130. if ($tokens[$index]->isClassy()) {
  131. $this->fixClassyDefinition($tokens, $index);
  132. }
  133. }
  134. }
  135. protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
  136. {
  137. return new FixerConfigurationResolver([
  138. (new FixerOptionBuilder('multi_line_extends_each_single_line', 'Whether definitions should be multiline.'))
  139. ->setAllowedTypes(['bool'])
  140. ->setDefault(false)
  141. ->getOption(),
  142. (new FixerOptionBuilder('single_item_single_line', 'Whether definitions should be single line when including a single item.'))
  143. ->setAllowedTypes(['bool'])
  144. ->setDefault(false)
  145. ->getOption(),
  146. (new FixerOptionBuilder('single_line', 'Whether definitions should be single line.'))
  147. ->setAllowedTypes(['bool'])
  148. ->setDefault(false)
  149. ->getOption(),
  150. (new FixerOptionBuilder('space_before_parenthesis', 'Whether there should be a single space after the parenthesis of anonymous class (PSR12) or not.'))
  151. ->setAllowedTypes(['bool'])
  152. ->setDefault(false)
  153. ->getOption(),
  154. (new FixerOptionBuilder('inline_constructor_arguments', 'Whether constructor argument list in anonymous classes should be single line.'))
  155. ->setAllowedTypes(['bool'])
  156. ->setDefault(true)
  157. ->getOption(),
  158. ]);
  159. }
  160. /**
  161. * @param int $classyIndex Class definition token start index
  162. */
  163. private function fixClassyDefinition(Tokens $tokens, int $classyIndex): void
  164. {
  165. $classDefInfo = $this->getClassyDefinitionInfo($tokens, $classyIndex);
  166. // PSR2 4.1 Lists of implements MAY be split across multiple lines, where each subsequent line is indented once.
  167. // When doing so, the first item in the list MUST be on the next line, and there MUST be only one interface per line.
  168. if (false !== $classDefInfo['implements']) {
  169. $classDefInfo['implements'] = $this->fixClassyDefinitionImplements(
  170. $tokens,
  171. $classDefInfo['open'],
  172. $classDefInfo['implements']
  173. );
  174. }
  175. if (false !== $classDefInfo['extends']) {
  176. $classDefInfo['extends'] = $this->fixClassyDefinitionExtends(
  177. $tokens,
  178. false === $classDefInfo['implements'] ? $classDefInfo['open'] : $classDefInfo['implements']['start'],
  179. $classDefInfo['extends']
  180. );
  181. }
  182. // PSR2: class definition open curly brace must go on a new line.
  183. // PSR12: anonymous class curly brace on same line if not multi line implements.
  184. $classDefInfo['open'] = $this->fixClassyDefinitionOpenSpacing($tokens, $classDefInfo);
  185. if ($classDefInfo['implements']) {
  186. $end = $classDefInfo['implements']['start'];
  187. } elseif ($classDefInfo['extends']) {
  188. $end = $classDefInfo['extends']['start'];
  189. } else {
  190. $end = $tokens->getPrevNonWhitespace($classDefInfo['open']);
  191. }
  192. if ($classDefInfo['anonymousClass'] && false === $this->configuration['inline_constructor_arguments']) {
  193. if (!$tokens[$end]->equals(')')) { // anonymous class with `extends` and/or `implements`
  194. $start = $tokens->getPrevMeaningfulToken($end);
  195. $this->makeClassyDefinitionSingleLine($tokens, $start, $end);
  196. $end = $start;
  197. }
  198. if ($tokens[$end]->equals(')')) { // skip constructor arguments of anonymous class
  199. $end = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $end);
  200. }
  201. }
  202. // 4.1 The extends and implements keywords MUST be declared on the same line as the class name.
  203. $this->makeClassyDefinitionSingleLine($tokens, $classDefInfo['start'], $end);
  204. $this->sortClassModifiers($tokens, $classDefInfo);
  205. }
  206. /**
  207. * @param _ClassExtendsInfo $classExtendsInfo
  208. *
  209. * @return _ClassExtendsInfo
  210. */
  211. private function fixClassyDefinitionExtends(Tokens $tokens, int $classOpenIndex, array $classExtendsInfo): array
  212. {
  213. $endIndex = $tokens->getPrevNonWhitespace($classOpenIndex);
  214. if (true === $this->configuration['single_line'] || false === $classExtendsInfo['multiLine']) {
  215. $this->makeClassyDefinitionSingleLine($tokens, $classExtendsInfo['start'], $endIndex);
  216. $classExtendsInfo['multiLine'] = false;
  217. } elseif (true === $this->configuration['single_item_single_line'] && 1 === $classExtendsInfo['numberOfExtends']) {
  218. $this->makeClassyDefinitionSingleLine($tokens, $classExtendsInfo['start'], $endIndex);
  219. $classExtendsInfo['multiLine'] = false;
  220. } elseif (true === $this->configuration['multi_line_extends_each_single_line'] && $classExtendsInfo['multiLine']) {
  221. $this->makeClassyInheritancePartMultiLine($tokens, $classExtendsInfo['start'], $endIndex);
  222. $classExtendsInfo['multiLine'] = true;
  223. }
  224. return $classExtendsInfo;
  225. }
  226. /**
  227. * @param _ClassImplementsInfo $classImplementsInfo
  228. *
  229. * @return _ClassImplementsInfo
  230. */
  231. private function fixClassyDefinitionImplements(Tokens $tokens, int $classOpenIndex, array $classImplementsInfo): array
  232. {
  233. $endIndex = $tokens->getPrevNonWhitespace($classOpenIndex);
  234. if (true === $this->configuration['single_line'] || false === $classImplementsInfo['multiLine']) {
  235. $this->makeClassyDefinitionSingleLine($tokens, $classImplementsInfo['start'], $endIndex);
  236. $classImplementsInfo['multiLine'] = false;
  237. } elseif (true === $this->configuration['single_item_single_line'] && 1 === $classImplementsInfo['numberOfImplements']) {
  238. $this->makeClassyDefinitionSingleLine($tokens, $classImplementsInfo['start'], $endIndex);
  239. $classImplementsInfo['multiLine'] = false;
  240. } else {
  241. $this->makeClassyInheritancePartMultiLine($tokens, $classImplementsInfo['start'], $endIndex);
  242. $classImplementsInfo['multiLine'] = true;
  243. }
  244. return $classImplementsInfo;
  245. }
  246. /**
  247. * @param array{
  248. * start: int,
  249. * classy: int,
  250. * open: int,
  251. * extends: false|_ClassExtendsInfo,
  252. * implements: false|_ClassImplementsInfo,
  253. * anonymousClass: bool,
  254. * final: false|int,
  255. * abstract: false|int,
  256. * readonly: false|int,
  257. * } $classDefInfo
  258. */
  259. private function fixClassyDefinitionOpenSpacing(Tokens $tokens, array $classDefInfo): int
  260. {
  261. if ($classDefInfo['anonymousClass']) {
  262. if (false !== $classDefInfo['implements']) {
  263. $spacing = $classDefInfo['implements']['multiLine'] ? $this->whitespacesConfig->getLineEnding() : ' ';
  264. } elseif (false !== $classDefInfo['extends']) {
  265. $spacing = $classDefInfo['extends']['multiLine'] ? $this->whitespacesConfig->getLineEnding() : ' ';
  266. } else {
  267. $spacing = ' ';
  268. }
  269. } else {
  270. $spacing = $this->whitespacesConfig->getLineEnding();
  271. }
  272. $openIndex = $tokens->getNextTokenOfKind($classDefInfo['classy'], ['{']);
  273. if (' ' !== $spacing && str_contains($tokens[$openIndex - 1]->getContent(), "\n")) {
  274. return $openIndex;
  275. }
  276. if ($tokens[$openIndex - 1]->isWhitespace()) {
  277. if (' ' !== $spacing || !$tokens[$tokens->getPrevNonWhitespace($openIndex - 1)]->isComment()) {
  278. $tokens[$openIndex - 1] = new Token([T_WHITESPACE, $spacing]);
  279. }
  280. return $openIndex;
  281. }
  282. $tokens->insertAt($openIndex, new Token([T_WHITESPACE, $spacing]));
  283. return $openIndex + 1;
  284. }
  285. /**
  286. * @return array{
  287. * start: int,
  288. * classy: int,
  289. * open: int,
  290. * extends: false|_ClassExtendsInfo,
  291. * implements: false|_ClassImplementsInfo,
  292. * anonymousClass: bool,
  293. * final: false|int,
  294. * abstract: false|int,
  295. * readonly: false|int,
  296. * }
  297. */
  298. private function getClassyDefinitionInfo(Tokens $tokens, int $classyIndex): array
  299. {
  300. $tokensAnalyzer = new TokensAnalyzer($tokens);
  301. $openIndex = $tokens->getNextTokenOfKind($classyIndex, ['{']);
  302. $def = [
  303. 'classy' => $classyIndex,
  304. 'open' => $openIndex,
  305. 'extends' => false,
  306. 'implements' => false,
  307. 'anonymousClass' => false,
  308. 'final' => false,
  309. 'abstract' => false,
  310. 'readonly' => false,
  311. ];
  312. if (!$tokens[$classyIndex]->isGivenKind(T_TRAIT)) {
  313. $extends = $tokens->findGivenKind(T_EXTENDS, $classyIndex, $openIndex);
  314. $def['extends'] = [] !== $extends ? $this->getClassyInheritanceInfo($tokens, array_key_first($extends), 'numberOfExtends') : false;
  315. if (!$tokens[$classyIndex]->isGivenKind(T_INTERFACE)) {
  316. $implements = $tokens->findGivenKind(T_IMPLEMENTS, $classyIndex, $openIndex);
  317. $def['implements'] = [] !== $implements ? $this->getClassyInheritanceInfo($tokens, array_key_first($implements), 'numberOfImplements') : false;
  318. $def['anonymousClass'] = $tokensAnalyzer->isAnonymousClass($classyIndex);
  319. }
  320. }
  321. if ($def['anonymousClass']) {
  322. $startIndex = $tokens->getPrevTokenOfKind($classyIndex, [[T_NEW]]); // go to "new" for anonymous class
  323. } else {
  324. $modifiers = $tokensAnalyzer->getClassyModifiers($classyIndex);
  325. $startIndex = $classyIndex;
  326. foreach (['final', 'abstract', 'readonly'] as $modifier) {
  327. if (isset($modifiers[$modifier])) {
  328. $def[$modifier] = $modifiers[$modifier];
  329. $startIndex = min($startIndex, $modifiers[$modifier]);
  330. } else {
  331. $def[$modifier] = false;
  332. }
  333. }
  334. }
  335. $def['start'] = $startIndex;
  336. return $def;
  337. }
  338. /**
  339. * @return array<string, 1>|array{start: int, multiLine: bool}
  340. */
  341. private function getClassyInheritanceInfo(Tokens $tokens, int $startIndex, string $label): array
  342. {
  343. $implementsInfo = ['start' => $startIndex, $label => 1, 'multiLine' => false];
  344. ++$startIndex;
  345. $endIndex = $tokens->getNextTokenOfKind($startIndex, ['{', [T_IMPLEMENTS], [T_EXTENDS]]);
  346. $endIndex = $tokens[$endIndex]->equals('{') ? $tokens->getPrevNonWhitespace($endIndex) : $endIndex;
  347. for ($i = $startIndex; $i < $endIndex; ++$i) {
  348. if ($tokens[$i]->equals(',')) {
  349. ++$implementsInfo[$label];
  350. continue;
  351. }
  352. if (!$implementsInfo['multiLine'] && str_contains($tokens[$i]->getContent(), "\n")) {
  353. $implementsInfo['multiLine'] = true;
  354. }
  355. }
  356. return $implementsInfo;
  357. }
  358. private function makeClassyDefinitionSingleLine(Tokens $tokens, int $startIndex, int $endIndex): void
  359. {
  360. for ($i = $endIndex; $i >= $startIndex; --$i) {
  361. if ($tokens[$i]->isWhitespace()) {
  362. if (str_contains($tokens[$i]->getContent(), "\n")) {
  363. if (\defined('T_ATTRIBUTE')) { // @TODO: drop condition and else when PHP 8.0+ is required
  364. if ($tokens[$i - 1]->isGivenKind(CT::T_ATTRIBUTE_CLOSE) || $tokens[$i + 1]->isGivenKind(T_ATTRIBUTE)) {
  365. continue;
  366. }
  367. } else {
  368. if (($tokens[$i - 1]->isComment() && str_ends_with($tokens[$i - 1]->getContent(), ']'))
  369. || ($tokens[$i + 1]->isComment() && str_starts_with($tokens[$i + 1]->getContent(), '#['))
  370. ) {
  371. continue;
  372. }
  373. }
  374. if ($tokens[$i - 1]->isGivenKind(T_DOC_COMMENT) || $tokens[$i + 1]->isGivenKind(T_DOC_COMMENT)) {
  375. continue;
  376. }
  377. }
  378. if ($tokens[$i - 1]->isComment()) {
  379. $content = $tokens[$i - 1]->getContent();
  380. if (!str_starts_with($content, '//') && !str_starts_with($content, '#')) {
  381. $tokens[$i] = new Token([T_WHITESPACE, ' ']);
  382. }
  383. continue;
  384. }
  385. if ($tokens[$i + 1]->isComment()) {
  386. $content = $tokens[$i + 1]->getContent();
  387. if (!str_starts_with($content, '//')) {
  388. $tokens[$i] = new Token([T_WHITESPACE, ' ']);
  389. }
  390. continue;
  391. }
  392. if ($tokens[$i - 1]->isGivenKind(T_CLASS) && $tokens[$i + 1]->equals('(')) {
  393. if (true === $this->configuration['space_before_parenthesis']) {
  394. $tokens[$i] = new Token([T_WHITESPACE, ' ']);
  395. } else {
  396. $tokens->clearAt($i);
  397. }
  398. continue;
  399. }
  400. if (!$tokens[$i - 1]->equals(',') && $tokens[$i + 1]->equalsAny([',', ')']) || $tokens[$i - 1]->equals('(')) {
  401. $tokens->clearAt($i);
  402. continue;
  403. }
  404. $tokens[$i] = new Token([T_WHITESPACE, ' ']);
  405. continue;
  406. }
  407. if ($tokens[$i]->equals(',') && !$tokens[$i + 1]->isWhitespace()) {
  408. $tokens->insertAt($i + 1, new Token([T_WHITESPACE, ' ']));
  409. continue;
  410. }
  411. if (true === $this->configuration['space_before_parenthesis'] && $tokens[$i]->isGivenKind(T_CLASS) && !$tokens[$i + 1]->isWhitespace()) {
  412. $tokens->insertAt($i + 1, new Token([T_WHITESPACE, ' ']));
  413. continue;
  414. }
  415. if (!$tokens[$i]->isComment()) {
  416. continue;
  417. }
  418. if (!$tokens[$i + 1]->isWhitespace() && !$tokens[$i + 1]->isComment() && !str_contains($tokens[$i]->getContent(), "\n")) {
  419. $tokens->insertAt($i + 1, new Token([T_WHITESPACE, ' ']));
  420. }
  421. if (!$tokens[$i - 1]->isWhitespace() && !$tokens[$i - 1]->isComment()) {
  422. $tokens->insertAt($i, new Token([T_WHITESPACE, ' ']));
  423. }
  424. }
  425. }
  426. private function makeClassyInheritancePartMultiLine(Tokens $tokens, int $startIndex, int $endIndex): void
  427. {
  428. for ($i = $endIndex; $i > $startIndex; --$i) {
  429. $previousInterfaceImplementingIndex = $tokens->getPrevTokenOfKind($i, [',', [T_IMPLEMENTS], [T_EXTENDS]]);
  430. $breakAtIndex = $tokens->getNextMeaningfulToken($previousInterfaceImplementingIndex);
  431. // make the part of a ',' or 'implements' single line
  432. $this->makeClassyDefinitionSingleLine(
  433. $tokens,
  434. $breakAtIndex,
  435. $i
  436. );
  437. // make sure the part is on its own line
  438. $isOnOwnLine = false;
  439. for ($j = $breakAtIndex; $j > $previousInterfaceImplementingIndex; --$j) {
  440. if (str_contains($tokens[$j]->getContent(), "\n")) {
  441. $isOnOwnLine = true;
  442. break;
  443. }
  444. }
  445. if (!$isOnOwnLine) {
  446. if ($tokens[$breakAtIndex - 1]->isWhitespace()) {
  447. $tokens[$breakAtIndex - 1] = new Token([
  448. T_WHITESPACE,
  449. $this->whitespacesConfig->getLineEnding().$this->whitespacesConfig->getIndent(),
  450. ]);
  451. } else {
  452. $tokens->insertAt($breakAtIndex, new Token([T_WHITESPACE, $this->whitespacesConfig->getLineEnding().$this->whitespacesConfig->getIndent()]));
  453. }
  454. }
  455. $i = $previousInterfaceImplementingIndex + 1;
  456. }
  457. }
  458. /**
  459. * @param array{
  460. * final: false|int,
  461. * abstract: false|int,
  462. * readonly: false|int,
  463. * } $classDefInfo
  464. */
  465. private function sortClassModifiers(Tokens $tokens, array $classDefInfo): void
  466. {
  467. if (false === $classDefInfo['readonly']) {
  468. return;
  469. }
  470. $readonlyIndex = $classDefInfo['readonly'];
  471. foreach (['final', 'abstract'] as $accessModifier) {
  472. if (false === $classDefInfo[$accessModifier] || $classDefInfo[$accessModifier] < $readonlyIndex) {
  473. continue;
  474. }
  475. $accessModifierIndex = $classDefInfo[$accessModifier];
  476. /** @var Token $readonlyToken */
  477. $readonlyToken = clone $tokens[$readonlyIndex];
  478. /** @var Token $accessToken */
  479. $accessToken = clone $tokens[$accessModifierIndex];
  480. $tokens[$readonlyIndex] = $accessToken;
  481. $tokens[$accessModifierIndex] = $readonlyToken;
  482. break;
  483. }
  484. }
  485. }