123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572 |
- <?php
- declare(strict_types=1);
- /*
- * This file is part of PHP CS Fixer.
- *
- * (c) Fabien Potencier <fabien@symfony.com>
- * Dariusz Rumiński <dariusz.ruminski@gmail.com>
- *
- * This source file is subject to the MIT license that is bundled
- * with this source code in the file LICENSE.
- */
- namespace PhpCsFixer\Fixer\ClassNotation;
- use PhpCsFixer\AbstractFixer;
- use PhpCsFixer\Fixer\ConfigurableFixerInterface;
- use PhpCsFixer\Fixer\ConfigurableFixerTrait;
- use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
- use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
- use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
- use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
- use PhpCsFixer\FixerDefinition\CodeSample;
- use PhpCsFixer\FixerDefinition\FixerDefinition;
- use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
- use PhpCsFixer\Tokenizer\CT;
- use PhpCsFixer\Tokenizer\Token;
- use PhpCsFixer\Tokenizer\Tokens;
- use PhpCsFixer\Tokenizer\TokensAnalyzer;
- /**
- * Fixer for part of the rules defined in PSR2 ¶4.1 Extends and Implements and PSR12 ¶8. Anonymous Classes.
- *
- * @phpstan-type _ClassExtendsInfo array{start: int, numberOfExtends: int, multiLine: bool}
- * @phpstan-type _ClassImplementsInfo array{start: int, numberOfImplements: int, multiLine: bool}
- *
- * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
- *
- * @phpstan-type _AutogeneratedInputConfiguration array{
- * inline_constructor_arguments?: bool,
- * multi_line_extends_each_single_line?: bool,
- * single_item_single_line?: bool,
- * single_line?: bool,
- * space_before_parenthesis?: bool
- * }
- * @phpstan-type _AutogeneratedComputedConfiguration array{
- * inline_constructor_arguments: bool,
- * multi_line_extends_each_single_line: bool,
- * single_item_single_line: bool,
- * single_line: bool,
- * space_before_parenthesis: bool
- * }
- */
- final class ClassDefinitionFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
- {
- /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
- use ConfigurableFixerTrait;
- public function getDefinition(): FixerDefinitionInterface
- {
- return new FixerDefinition(
- 'Whitespace around the keywords of a class, trait, enum or interfaces definition should be one space.',
- [
- new CodeSample(
- '<?php
- class Foo extends Bar implements Baz, BarBaz
- {
- }
- final class Foo extends Bar implements Baz, BarBaz
- {
- }
- trait Foo
- {
- }
- $foo = new class extends Bar implements Baz, BarBaz {};
- '
- ),
- new CodeSample(
- '<?php
- class Foo
- extends Bar
- implements Baz, BarBaz
- {}
- ',
- ['single_line' => true]
- ),
- new CodeSample(
- '<?php
- class Foo
- extends Bar
- implements Baz
- {}
- ',
- ['single_item_single_line' => true]
- ),
- new CodeSample(
- '<?php
- interface Bar extends
- Bar, BarBaz, FooBarBaz
- {}
- ',
- ['multi_line_extends_each_single_line' => true]
- ),
- new CodeSample(
- '<?php
- $foo = new class(){};
- ',
- ['space_before_parenthesis' => true]
- ),
- new CodeSample(
- "<?php\n\$foo = new class(\n \$bar,\n \$baz\n) {};\n",
- ['inline_constructor_arguments' => true]
- ),
- ]
- );
- }
- /**
- * {@inheritdoc}
- *
- * Must run before BracesFixer, SingleLineEmptyBodyFixer.
- * Must run after NewWithBracesFixer, NewWithParenthesesFixer.
- */
- public function getPriority(): int
- {
- return 36;
- }
- public function isCandidate(Tokens $tokens): bool
- {
- return $tokens->isAnyTokenKindsFound(Token::getClassyTokenKinds());
- }
- protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
- {
- // -4, one for count to index, 3 because min. of tokens for a classy location.
- for ($index = $tokens->getSize() - 4; $index > 0; --$index) {
- if ($tokens[$index]->isClassy()) {
- $this->fixClassyDefinition($tokens, $index);
- }
- }
- }
- protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
- {
- return new FixerConfigurationResolver([
- (new FixerOptionBuilder('multi_line_extends_each_single_line', 'Whether definitions should be multiline.'))
- ->setAllowedTypes(['bool'])
- ->setDefault(false)
- ->getOption(),
- (new FixerOptionBuilder('single_item_single_line', 'Whether definitions should be single line when including a single item.'))
- ->setAllowedTypes(['bool'])
- ->setDefault(false)
- ->getOption(),
- (new FixerOptionBuilder('single_line', 'Whether definitions should be single line.'))
- ->setAllowedTypes(['bool'])
- ->setDefault(false)
- ->getOption(),
- (new FixerOptionBuilder('space_before_parenthesis', 'Whether there should be a single space after the parenthesis of anonymous class (PSR12) or not.'))
- ->setAllowedTypes(['bool'])
- ->setDefault(false)
- ->getOption(),
- (new FixerOptionBuilder('inline_constructor_arguments', 'Whether constructor argument list in anonymous classes should be single line.'))
- ->setAllowedTypes(['bool'])
- ->setDefault(true)
- ->getOption(),
- ]);
- }
- /**
- * @param int $classyIndex Class definition token start index
- */
- private function fixClassyDefinition(Tokens $tokens, int $classyIndex): void
- {
- $classDefInfo = $this->getClassyDefinitionInfo($tokens, $classyIndex);
- // PSR2 4.1 Lists of implements MAY be split across multiple lines, where each subsequent line is indented once.
- // When doing so, the first item in the list MUST be on the next line, and there MUST be only one interface per line.
- if (false !== $classDefInfo['implements']) {
- $classDefInfo['implements'] = $this->fixClassyDefinitionImplements(
- $tokens,
- $classDefInfo['open'],
- $classDefInfo['implements']
- );
- }
- if (false !== $classDefInfo['extends']) {
- $classDefInfo['extends'] = $this->fixClassyDefinitionExtends(
- $tokens,
- false === $classDefInfo['implements'] ? $classDefInfo['open'] : $classDefInfo['implements']['start'],
- $classDefInfo['extends']
- );
- }
- // PSR2: class definition open curly brace must go on a new line.
- // PSR12: anonymous class curly brace on same line if not multi line implements.
- $classDefInfo['open'] = $this->fixClassyDefinitionOpenSpacing($tokens, $classDefInfo);
- if ($classDefInfo['implements']) {
- $end = $classDefInfo['implements']['start'];
- } elseif ($classDefInfo['extends']) {
- $end = $classDefInfo['extends']['start'];
- } else {
- $end = $tokens->getPrevNonWhitespace($classDefInfo['open']);
- }
- if ($classDefInfo['anonymousClass'] && false === $this->configuration['inline_constructor_arguments']) {
- if (!$tokens[$end]->equals(')')) { // anonymous class with `extends` and/or `implements`
- $start = $tokens->getPrevMeaningfulToken($end);
- $this->makeClassyDefinitionSingleLine($tokens, $start, $end);
- $end = $start;
- }
- if ($tokens[$end]->equals(')')) { // skip constructor arguments of anonymous class
- $end = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $end);
- }
- }
- // 4.1 The extends and implements keywords MUST be declared on the same line as the class name.
- $this->makeClassyDefinitionSingleLine($tokens, $classDefInfo['start'], $end);
- $this->sortClassModifiers($tokens, $classDefInfo);
- }
- /**
- * @param _ClassExtendsInfo $classExtendsInfo
- *
- * @return _ClassExtendsInfo
- */
- private function fixClassyDefinitionExtends(Tokens $tokens, int $classOpenIndex, array $classExtendsInfo): array
- {
- $endIndex = $tokens->getPrevNonWhitespace($classOpenIndex);
- if (true === $this->configuration['single_line'] || false === $classExtendsInfo['multiLine']) {
- $this->makeClassyDefinitionSingleLine($tokens, $classExtendsInfo['start'], $endIndex);
- $classExtendsInfo['multiLine'] = false;
- } elseif (true === $this->configuration['single_item_single_line'] && 1 === $classExtendsInfo['numberOfExtends']) {
- $this->makeClassyDefinitionSingleLine($tokens, $classExtendsInfo['start'], $endIndex);
- $classExtendsInfo['multiLine'] = false;
- } elseif (true === $this->configuration['multi_line_extends_each_single_line'] && $classExtendsInfo['multiLine']) {
- $this->makeClassyInheritancePartMultiLine($tokens, $classExtendsInfo['start'], $endIndex);
- $classExtendsInfo['multiLine'] = true;
- }
- return $classExtendsInfo;
- }
- /**
- * @param _ClassImplementsInfo $classImplementsInfo
- *
- * @return _ClassImplementsInfo
- */
- private function fixClassyDefinitionImplements(Tokens $tokens, int $classOpenIndex, array $classImplementsInfo): array
- {
- $endIndex = $tokens->getPrevNonWhitespace($classOpenIndex);
- if (true === $this->configuration['single_line'] || false === $classImplementsInfo['multiLine']) {
- $this->makeClassyDefinitionSingleLine($tokens, $classImplementsInfo['start'], $endIndex);
- $classImplementsInfo['multiLine'] = false;
- } elseif (true === $this->configuration['single_item_single_line'] && 1 === $classImplementsInfo['numberOfImplements']) {
- $this->makeClassyDefinitionSingleLine($tokens, $classImplementsInfo['start'], $endIndex);
- $classImplementsInfo['multiLine'] = false;
- } else {
- $this->makeClassyInheritancePartMultiLine($tokens, $classImplementsInfo['start'], $endIndex);
- $classImplementsInfo['multiLine'] = true;
- }
- return $classImplementsInfo;
- }
- /**
- * @param array{
- * start: int,
- * classy: int,
- * open: int,
- * extends: false|_ClassExtendsInfo,
- * implements: false|_ClassImplementsInfo,
- * anonymousClass: bool,
- * final: false|int,
- * abstract: false|int,
- * readonly: false|int,
- * } $classDefInfo
- */
- private function fixClassyDefinitionOpenSpacing(Tokens $tokens, array $classDefInfo): int
- {
- if ($classDefInfo['anonymousClass']) {
- if (false !== $classDefInfo['implements']) {
- $spacing = $classDefInfo['implements']['multiLine'] ? $this->whitespacesConfig->getLineEnding() : ' ';
- } elseif (false !== $classDefInfo['extends']) {
- $spacing = $classDefInfo['extends']['multiLine'] ? $this->whitespacesConfig->getLineEnding() : ' ';
- } else {
- $spacing = ' ';
- }
- } else {
- $spacing = $this->whitespacesConfig->getLineEnding();
- }
- $openIndex = $tokens->getNextTokenOfKind($classDefInfo['classy'], ['{']);
- if (' ' !== $spacing && str_contains($tokens[$openIndex - 1]->getContent(), "\n")) {
- return $openIndex;
- }
- if ($tokens[$openIndex - 1]->isWhitespace()) {
- if (' ' !== $spacing || !$tokens[$tokens->getPrevNonWhitespace($openIndex - 1)]->isComment()) {
- $tokens[$openIndex - 1] = new Token([T_WHITESPACE, $spacing]);
- }
- return $openIndex;
- }
- $tokens->insertAt($openIndex, new Token([T_WHITESPACE, $spacing]));
- return $openIndex + 1;
- }
- /**
- * @return array{
- * start: int,
- * classy: int,
- * open: int,
- * extends: false|_ClassExtendsInfo,
- * implements: false|_ClassImplementsInfo,
- * anonymousClass: bool,
- * final: false|int,
- * abstract: false|int,
- * readonly: false|int,
- * }
- */
- private function getClassyDefinitionInfo(Tokens $tokens, int $classyIndex): array
- {
- $tokensAnalyzer = new TokensAnalyzer($tokens);
- $openIndex = $tokens->getNextTokenOfKind($classyIndex, ['{']);
- $def = [
- 'classy' => $classyIndex,
- 'open' => $openIndex,
- 'extends' => false,
- 'implements' => false,
- 'anonymousClass' => false,
- 'final' => false,
- 'abstract' => false,
- 'readonly' => false,
- ];
- if (!$tokens[$classyIndex]->isGivenKind(T_TRAIT)) {
- $extends = $tokens->findGivenKind(T_EXTENDS, $classyIndex, $openIndex);
- $def['extends'] = [] !== $extends ? $this->getClassyInheritanceInfo($tokens, array_key_first($extends), 'numberOfExtends') : false;
- if (!$tokens[$classyIndex]->isGivenKind(T_INTERFACE)) {
- $implements = $tokens->findGivenKind(T_IMPLEMENTS, $classyIndex, $openIndex);
- $def['implements'] = [] !== $implements ? $this->getClassyInheritanceInfo($tokens, array_key_first($implements), 'numberOfImplements') : false;
- $def['anonymousClass'] = $tokensAnalyzer->isAnonymousClass($classyIndex);
- }
- }
- if ($def['anonymousClass']) {
- $startIndex = $tokens->getPrevTokenOfKind($classyIndex, [[T_NEW]]); // go to "new" for anonymous class
- } else {
- $modifiers = $tokensAnalyzer->getClassyModifiers($classyIndex);
- $startIndex = $classyIndex;
- foreach (['final', 'abstract', 'readonly'] as $modifier) {
- if (isset($modifiers[$modifier])) {
- $def[$modifier] = $modifiers[$modifier];
- $startIndex = min($startIndex, $modifiers[$modifier]);
- } else {
- $def[$modifier] = false;
- }
- }
- }
- $def['start'] = $startIndex;
- return $def;
- }
- /**
- * @return array<string, 1>|array{start: int, multiLine: bool}
- */
- private function getClassyInheritanceInfo(Tokens $tokens, int $startIndex, string $label): array
- {
- $implementsInfo = ['start' => $startIndex, $label => 1, 'multiLine' => false];
- ++$startIndex;
- $endIndex = $tokens->getNextTokenOfKind($startIndex, ['{', [T_IMPLEMENTS], [T_EXTENDS]]);
- $endIndex = $tokens[$endIndex]->equals('{') ? $tokens->getPrevNonWhitespace($endIndex) : $endIndex;
- for ($i = $startIndex; $i < $endIndex; ++$i) {
- if ($tokens[$i]->equals(',')) {
- ++$implementsInfo[$label];
- continue;
- }
- if (!$implementsInfo['multiLine'] && str_contains($tokens[$i]->getContent(), "\n")) {
- $implementsInfo['multiLine'] = true;
- }
- }
- return $implementsInfo;
- }
- private function makeClassyDefinitionSingleLine(Tokens $tokens, int $startIndex, int $endIndex): void
- {
- for ($i = $endIndex; $i >= $startIndex; --$i) {
- if ($tokens[$i]->isWhitespace()) {
- if (str_contains($tokens[$i]->getContent(), "\n")) {
- if (\defined('T_ATTRIBUTE')) { // @TODO: drop condition and else when PHP 8.0+ is required
- if ($tokens[$i - 1]->isGivenKind(CT::T_ATTRIBUTE_CLOSE) || $tokens[$i + 1]->isGivenKind(T_ATTRIBUTE)) {
- continue;
- }
- } else {
- if (($tokens[$i - 1]->isComment() && str_ends_with($tokens[$i - 1]->getContent(), ']'))
- || ($tokens[$i + 1]->isComment() && str_starts_with($tokens[$i + 1]->getContent(), '#['))
- ) {
- continue;
- }
- }
- if ($tokens[$i - 1]->isGivenKind(T_DOC_COMMENT) || $tokens[$i + 1]->isGivenKind(T_DOC_COMMENT)) {
- continue;
- }
- }
- if ($tokens[$i - 1]->isComment()) {
- $content = $tokens[$i - 1]->getContent();
- if (!str_starts_with($content, '//') && !str_starts_with($content, '#')) {
- $tokens[$i] = new Token([T_WHITESPACE, ' ']);
- }
- continue;
- }
- if ($tokens[$i + 1]->isComment()) {
- $content = $tokens[$i + 1]->getContent();
- if (!str_starts_with($content, '//')) {
- $tokens[$i] = new Token([T_WHITESPACE, ' ']);
- }
- continue;
- }
- if ($tokens[$i - 1]->isGivenKind(T_CLASS) && $tokens[$i + 1]->equals('(')) {
- if (true === $this->configuration['space_before_parenthesis']) {
- $tokens[$i] = new Token([T_WHITESPACE, ' ']);
- } else {
- $tokens->clearAt($i);
- }
- continue;
- }
- if (!$tokens[$i - 1]->equals(',') && $tokens[$i + 1]->equalsAny([',', ')']) || $tokens[$i - 1]->equals('(')) {
- $tokens->clearAt($i);
- continue;
- }
- $tokens[$i] = new Token([T_WHITESPACE, ' ']);
- continue;
- }
- if ($tokens[$i]->equals(',') && !$tokens[$i + 1]->isWhitespace()) {
- $tokens->insertAt($i + 1, new Token([T_WHITESPACE, ' ']));
- continue;
- }
- if (true === $this->configuration['space_before_parenthesis'] && $tokens[$i]->isGivenKind(T_CLASS) && !$tokens[$i + 1]->isWhitespace()) {
- $tokens->insertAt($i + 1, new Token([T_WHITESPACE, ' ']));
- continue;
- }
- if (!$tokens[$i]->isComment()) {
- continue;
- }
- if (!$tokens[$i + 1]->isWhitespace() && !$tokens[$i + 1]->isComment() && !str_contains($tokens[$i]->getContent(), "\n")) {
- $tokens->insertAt($i + 1, new Token([T_WHITESPACE, ' ']));
- }
- if (!$tokens[$i - 1]->isWhitespace() && !$tokens[$i - 1]->isComment()) {
- $tokens->insertAt($i, new Token([T_WHITESPACE, ' ']));
- }
- }
- }
- private function makeClassyInheritancePartMultiLine(Tokens $tokens, int $startIndex, int $endIndex): void
- {
- for ($i = $endIndex; $i > $startIndex; --$i) {
- $previousInterfaceImplementingIndex = $tokens->getPrevTokenOfKind($i, [',', [T_IMPLEMENTS], [T_EXTENDS]]);
- $breakAtIndex = $tokens->getNextMeaningfulToken($previousInterfaceImplementingIndex);
- // make the part of a ',' or 'implements' single line
- $this->makeClassyDefinitionSingleLine(
- $tokens,
- $breakAtIndex,
- $i
- );
- // make sure the part is on its own line
- $isOnOwnLine = false;
- for ($j = $breakAtIndex; $j > $previousInterfaceImplementingIndex; --$j) {
- if (str_contains($tokens[$j]->getContent(), "\n")) {
- $isOnOwnLine = true;
- break;
- }
- }
- if (!$isOnOwnLine) {
- if ($tokens[$breakAtIndex - 1]->isWhitespace()) {
- $tokens[$breakAtIndex - 1] = new Token([
- T_WHITESPACE,
- $this->whitespacesConfig->getLineEnding().$this->whitespacesConfig->getIndent(),
- ]);
- } else {
- $tokens->insertAt($breakAtIndex, new Token([T_WHITESPACE, $this->whitespacesConfig->getLineEnding().$this->whitespacesConfig->getIndent()]));
- }
- }
- $i = $previousInterfaceImplementingIndex + 1;
- }
- }
- /**
- * @param array{
- * final: false|int,
- * abstract: false|int,
- * readonly: false|int,
- * } $classDefInfo
- */
- private function sortClassModifiers(Tokens $tokens, array $classDefInfo): void
- {
- if (false === $classDefInfo['readonly']) {
- return;
- }
- $readonlyIndex = $classDefInfo['readonly'];
- foreach (['final', 'abstract'] as $accessModifier) {
- if (false === $classDefInfo[$accessModifier] || $classDefInfo[$accessModifier] < $readonlyIndex) {
- continue;
- }
- $accessModifierIndex = $classDefInfo[$accessModifier];
- /** @var Token $readonlyToken */
- $readonlyToken = clone $tokens[$readonlyIndex];
- /** @var Token $accessToken */
- $accessToken = clone $tokens[$accessModifierIndex];
- $tokens[$readonlyIndex] = $accessToken;
- $tokens[$accessModifierIndex] = $readonlyToken;
- break;
- }
- }
- }
|