Fix bin/publish: copy docs.dist from project root

Fix bin/publish: use correct .env path for rspade_system
Fix bin/publish script: prevent grep exit code 1 from terminating script

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2025-10-21 02:08:33 +00:00
commit f6fac6c4bc
79758 changed files with 10547827 additions and 0 deletions

View File

@@ -0,0 +1,600 @@
<?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\FixerDefinition\VersionSpecification;
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
use PhpCsFixer\Utils;
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
/**
* Make sure there is one blank line above and below class elements.
*
* The exception is when an element is the first or last item in a 'classy'.
*
* @phpstan-type _Class array{
* index: int,
* open: int,
* close: int,
* elements: non-empty-list<_Element>
* }
* @phpstan-type _Element array{token: Token, type: string, index: int, start?: int, end?: int}
* @phpstan-type _AutogeneratedInputConfiguration array{
* elements?: array<string, string>,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* elements: array<string, string>,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*/
final class ClassAttributesSeparationFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
/**
* @internal
*/
public const SPACING_NONE = 'none';
/**
* @internal
*/
public const SPACING_ONE = 'one';
private const SPACING_ONLY_IF_META = 'only_if_meta';
private const MODIFIER_TYPES = [\T_PRIVATE, \T_PROTECTED, \T_PUBLIC, \T_ABSTRACT, \T_FINAL, \T_STATIC, \T_STRING, \T_NS_SEPARATOR, \T_VAR, CT::T_NULLABLE_TYPE, CT::T_ARRAY_TYPEHINT, CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION, CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_OPEN, CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_CLOSE, FCT::T_READONLY, FCT::T_PRIVATE_SET, FCT::T_PROTECTED_SET, FCT::T_PUBLIC_SET];
/**
* @var array<string, string>
*/
private array $classElementTypes = [];
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Class, trait and interface elements must be separated with one or none blank line.',
[
new CodeSample(
'<?php
final class Sample
{
protected function foo()
{
}
protected function bar()
{
}
}
'
),
new CodeSample(
'<?php
class Sample
{private $a; // foo
/** second in a hour */
private $b;
}
',
['elements' => ['property' => self::SPACING_ONE]]
),
new CodeSample(
'<?php
class Sample
{
const A = 1;
/** seconds in some hours */
const B = 3600;
}
',
['elements' => ['const' => self::SPACING_ONE]]
),
new CodeSample(
'<?php
class Sample
{
/** @var int */
const SECOND = 1;
/** @var int */
const MINUTE = 60;
const HOUR = 3600;
const DAY = 86400;
}
',
['elements' => ['const' => self::SPACING_ONLY_IF_META]]
),
new VersionSpecificCodeSample(
'<?php
class Sample
{
public $a;
#[SetUp]
public $b;
/** @var string */
public $c;
/** @internal */
#[Assert\String()]
public $d;
public $e;
}
',
new VersionSpecification(8_00_00),
['elements' => ['property' => self::SPACING_ONLY_IF_META]]
),
]
);
}
/**
* {@inheritdoc}
*
* Must run before BracesFixer, IndentationTypeFixer, NoExtraBlankLinesFixer, StatementIndentationFixer.
* Must run after OrderedClassElementsFixer, PhpUnitDataProviderMethodOrderFixer, SingleClassElementPerStatementFixer, VisibilityRequiredFixer.
*/
public function getPriority(): int
{
return 55;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound(Token::getClassyTokenKinds());
}
protected function configurePostNormalisation(): void
{
$this->classElementTypes = []; // reset previous configuration
foreach ($this->configuration['elements'] as $elementType => $spacing) {
$this->classElementTypes[$elementType] = $spacing;
}
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
foreach ($this->getElementsByClass($tokens) as $class) {
$elements = $class['elements'];
$elementCount = \count($elements);
if (0 === $elementCount) {
continue;
}
if (isset($this->classElementTypes[$elements[0]['type']])) {
$this->fixSpaceBelowClassElement($tokens, $class);
$this->fixSpaceAboveClassElement($tokens, $class, 0);
}
for ($index = 1; $index < $elementCount; ++$index) {
if (isset($this->classElementTypes[$elements[$index]['type']])) {
$this->fixSpaceAboveClassElement($tokens, $class, $index);
}
}
}
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('elements', 'Dictionary of `const|method|property|trait_import|case` => `none|one|only_if_meta` values.'))
->setAllowedTypes(['array<string, string>'])
->setAllowedValues([static function (array $option): bool {
foreach ($option as $type => $spacing) {
$supportedTypes = ['const', 'method', 'property', 'trait_import', 'case'];
if (!\in_array($type, $supportedTypes, true)) {
throw new InvalidOptionsException(
\sprintf(
'Unexpected element type, expected any of %s, got "%s".',
Utils::naturalLanguageJoin($supportedTypes),
\gettype($type).'#'.$type
)
);
}
$supportedSpacings = [self::SPACING_NONE, self::SPACING_ONE, self::SPACING_ONLY_IF_META];
if (!\in_array($spacing, $supportedSpacings, true)) {
throw new InvalidOptionsException(
\sprintf(
'Unexpected spacing for element type "%s", expected any of %s, got "%s".',
$spacing,
Utils::naturalLanguageJoin($supportedSpacings),
\is_object($spacing) ? \get_class($spacing) : (null === $spacing ? 'null' : \gettype($spacing).'#'.$spacing)
)
);
}
}
return true;
}])
->setDefault([
'const' => self::SPACING_ONE,
'method' => self::SPACING_ONE,
'property' => self::SPACING_ONE,
'trait_import' => self::SPACING_NONE,
'case' => self::SPACING_NONE,
])
->getOption(),
]);
}
/**
* Fix spacing above an element of a class, interface or trait.
*
* Deals with comments, PHPDocs and spaces above the element with respect to the position of the
* element within the class, interface or trait.
*
* @param _Class $class
*/
private function fixSpaceAboveClassElement(Tokens $tokens, array $class, int $elementIndex): void
{
$element = $class['elements'][$elementIndex];
$elementAboveEnd = isset($class['elements'][$elementIndex + 1]) ? $class['elements'][$elementIndex + 1]['end'] : 0;
$nonWhiteAbove = $tokens->getPrevNonWhitespace($element['start']);
// element is directly after class open brace
if ($nonWhiteAbove === $class['open']) {
$this->correctLineBreaks($tokens, $nonWhiteAbove, $element['start'], 1);
return;
}
// deal with comments above an element
if ($tokens[$nonWhiteAbove]->isGivenKind(\T_COMMENT)) {
// check if the comment belongs to the previous element
if ($elementAboveEnd === $nonWhiteAbove) {
$this->correctLineBreaks($tokens, $nonWhiteAbove, $element['start'], $this->determineRequiredLineCount($tokens, $class, $elementIndex));
return;
}
// more than one line break, always bring it back to 2 line breaks between the element start and what is above it
if ($tokens[$nonWhiteAbove + 1]->isWhitespace() && substr_count($tokens[$nonWhiteAbove + 1]->getContent(), "\n") > 1) {
$this->correctLineBreaks($tokens, $nonWhiteAbove, $element['start'], 2);
return;
}
// there are 2 cases:
if (
1 === $element['start'] - $nonWhiteAbove
|| $tokens[$nonWhiteAbove - 1]->isWhitespace() && substr_count($tokens[$nonWhiteAbove - 1]->getContent(), "\n") > 0
|| $tokens[$nonWhiteAbove + 1]->isWhitespace() && substr_count($tokens[$nonWhiteAbove + 1]->getContent(), "\n") > 0
) {
// 1. The comment is meant for the element (although not a PHPDoc),
// make sure there is one line break between the element and the comment...
$this->correctLineBreaks($tokens, $nonWhiteAbove, $element['start'], 1);
// ... and make sure there is blank line above the comment (with the exception when it is directly after a class opening)
$nonWhiteAbove = $this->findCommentBlockStart($tokens, $nonWhiteAbove, $elementAboveEnd);
$nonWhiteAboveComment = $tokens->getPrevNonWhitespace($nonWhiteAbove);
if ($nonWhiteAboveComment === $class['open']) {
if ($tokens[$nonWhiteAboveComment - 1]->isWhitespace() && substr_count($tokens[$nonWhiteAboveComment - 1]->getContent(), "\n") > 0) {
$this->correctLineBreaks($tokens, $nonWhiteAboveComment, $nonWhiteAbove, 1);
}
} else {
$this->correctLineBreaks($tokens, $nonWhiteAboveComment, $nonWhiteAbove, 2);
}
} else {
// 2. The comment belongs to the code above the element,
// make sure there is a blank line above the element (i.e. 2 line breaks)
$this->correctLineBreaks($tokens, $nonWhiteAbove, $element['start'], 2);
}
return;
}
// deal with element with a PHPDoc/attribute above it
if ($tokens[$nonWhiteAbove]->isGivenKind([\T_DOC_COMMENT, CT::T_ATTRIBUTE_CLOSE])) {
// there should be one linebreak between the element and the attribute above it
$this->correctLineBreaks($tokens, $nonWhiteAbove, $element['start'], 1);
// make sure there is blank line above the comment (with the exception when it is directly after a class opening)
$nonWhiteAbove = $this->findCommentBlockStart($tokens, $nonWhiteAbove, $elementAboveEnd);
$nonWhiteAboveComment = $tokens->getPrevNonWhitespace($nonWhiteAbove);
$this->correctLineBreaks($tokens, $nonWhiteAboveComment, $nonWhiteAbove, $nonWhiteAboveComment === $class['open'] ? 1 : 2);
return;
}
$this->correctLineBreaks($tokens, $nonWhiteAbove, $element['start'], $this->determineRequiredLineCount($tokens, $class, $elementIndex));
}
/**
* @param _Class $class
*/
private function determineRequiredLineCount(Tokens $tokens, array $class, int $elementIndex): int
{
$type = $class['elements'][$elementIndex]['type'];
$spacing = $this->classElementTypes[$type];
if (self::SPACING_ONE === $spacing) {
return 2;
}
if (self::SPACING_NONE === $spacing) {
if (!isset($class['elements'][$elementIndex + 1])) {
return 1;
}
$aboveElement = $class['elements'][$elementIndex + 1];
if ($aboveElement['type'] !== $type) {
return 2;
}
$aboveElementDocCandidateIndex = $tokens->getPrevNonWhitespace($aboveElement['start']);
return $tokens[$aboveElementDocCandidateIndex]->isGivenKind([\T_DOC_COMMENT, CT::T_ATTRIBUTE_CLOSE]) ? 2 : 1;
}
if (self::SPACING_ONLY_IF_META === $spacing) {
$aboveElementDocCandidateIndex = $tokens->getPrevNonWhitespace($class['elements'][$elementIndex]['start']);
return $tokens[$aboveElementDocCandidateIndex]->isGivenKind([\T_DOC_COMMENT, CT::T_ATTRIBUTE_CLOSE]) ? 2 : 1;
}
throw new \RuntimeException(\sprintf('Unknown spacing "%s".', $spacing));
}
/**
* @param _Class $class
*/
private function fixSpaceBelowClassElement(Tokens $tokens, array $class): void
{
$element = $class['elements'][0];
// if this is last element fix; fix to the class end `}` here if appropriate
if ($class['close'] === $tokens->getNextNonWhitespace($element['end'])) {
$this->correctLineBreaks($tokens, $element['end'], $class['close'], 1);
}
}
private function correctLineBreaks(Tokens $tokens, int $startIndex, int $endIndex, int $reqLineCount): void
{
$lineEnding = $this->whitespacesConfig->getLineEnding();
++$startIndex;
$numbOfWhiteTokens = $endIndex - $startIndex;
if (0 === $numbOfWhiteTokens) {
$tokens->insertAt($startIndex, new Token([\T_WHITESPACE, str_repeat($lineEnding, $reqLineCount)]));
return;
}
$lineBreakCount = $this->getLineBreakCount($tokens, $startIndex, $endIndex);
if ($reqLineCount === $lineBreakCount) {
return;
}
if ($lineBreakCount < $reqLineCount) {
$tokens[$startIndex] = new Token([
\T_WHITESPACE,
str_repeat($lineEnding, $reqLineCount - $lineBreakCount).$tokens[$startIndex]->getContent(),
]);
return;
}
// $lineCount = > $reqLineCount : check the one Token case first since this one will be true most of the time
if (1 === $numbOfWhiteTokens) {
$tokens[$startIndex] = new Token([
\T_WHITESPACE,
Preg::replace('/\r\n|\n/', '', $tokens[$startIndex]->getContent(), $lineBreakCount - $reqLineCount),
]);
return;
}
// $numbOfWhiteTokens = > 1
$toReplaceCount = $lineBreakCount - $reqLineCount;
for ($i = $startIndex; $i < $endIndex && $toReplaceCount > 0; ++$i) {
$tokenLineCount = substr_count($tokens[$i]->getContent(), "\n");
if ($tokenLineCount > 0) {
$tokens[$i] = new Token([
\T_WHITESPACE,
Preg::replace('/\r\n|\n/', '', $tokens[$i]->getContent(), min($toReplaceCount, $tokenLineCount)),
]);
$toReplaceCount -= $tokenLineCount;
}
}
}
private function getLineBreakCount(Tokens $tokens, int $startIndex, int $endIndex): int
{
$lineCount = 0;
for ($i = $startIndex; $i < $endIndex; ++$i) {
$lineCount += substr_count($tokens[$i]->getContent(), "\n");
}
return $lineCount;
}
private function findCommentBlockStart(Tokens $tokens, int $start, int $elementAboveEnd): int
{
for ($i = $start; $i > $elementAboveEnd; --$i) {
if ($tokens[$i]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) {
$start = $i = $tokens->findBlockStart(Tokens::BLOCK_TYPE_ATTRIBUTE, $i);
continue;
}
if ($tokens[$i]->isComment()) {
$start = $i;
continue;
}
if (!$tokens[$i]->isWhitespace() || $this->getLineBreakCount($tokens, $i, $i + 1) > 1) {
break;
}
}
return $start;
}
/**
* @TODO Introduce proper DTO instead of an array
*
* @return \Generator<_Class>
*/
private function getElementsByClass(Tokens $tokens): \Generator
{
$tokensAnalyzer = new TokensAnalyzer($tokens);
$class = $classIndex = false;
foreach (array_reverse($tokensAnalyzer->getClassyElements(), true) as $index => $element) {
$element['index'] = $index;
if ($element['classIndex'] !== $classIndex) {
if (false !== $class) {
yield $class;
}
$classIndex = $element['classIndex'];
$classOpen = $tokens->getNextTokenOfKind($classIndex, ['{']);
$classEnd = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $classOpen);
$class = [
'index' => $classIndex,
'open' => $classOpen,
'close' => $classEnd,
'elements' => [],
];
}
unset($element['classIndex']);
$element['start'] = $this->getFirstTokenIndexOfClassElement($tokens, $class, $element);
$element['end'] = $this->getLastTokenIndexOfClassElement($tokens, $class, $element, $tokensAnalyzer);
$class['elements'][] = $element; // reset the key by design
}
if (false !== $class) {
yield $class;
}
}
/**
* including trailing single line comments if belonging to the class element.
*
* @param _Class $class
* @param _Element $element
*/
private function getFirstTokenIndexOfClassElement(Tokens $tokens, array $class, array $element): int
{
$firstElementAttributeIndex = $element['index'];
do {
$nonWhiteAbove = $tokens->getPrevMeaningfulToken($firstElementAttributeIndex);
if (null !== $nonWhiteAbove && $tokens[$nonWhiteAbove]->isGivenKind(self::MODIFIER_TYPES)) {
$firstElementAttributeIndex = $nonWhiteAbove;
} else {
break;
}
} while ($firstElementAttributeIndex > $class['open']);
return $firstElementAttributeIndex;
}
/**
* including trailing single line comments if belonging to the class element.
*
* @param _Class $class
* @param _Element $element
*/
private function getLastTokenIndexOfClassElement(Tokens $tokens, array $class, array $element, TokensAnalyzer $tokensAnalyzer): int
{
// find last token of the element
if ('method' === $element['type'] && !$tokens[$class['index']]->isGivenKind(\T_INTERFACE)) {
$attributes = $tokensAnalyzer->getMethodAttributes($element['index']);
if (true === $attributes['abstract']) {
$elementEndIndex = $tokens->getNextTokenOfKind($element['index'], [';']);
} else {
$elementEndIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $tokens->getNextTokenOfKind($element['index'], ['{']));
}
} elseif ('trait_import' === $element['type']) {
$elementEndIndex = $element['index'];
do {
$elementEndIndex = $tokens->getNextMeaningfulToken($elementEndIndex);
} while ($tokens[$elementEndIndex]->isGivenKind([\T_STRING, \T_NS_SEPARATOR]) || $tokens[$elementEndIndex]->equals(','));
if (!$tokens[$elementEndIndex]->equals(';')) {
$elementEndIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $tokens->getNextTokenOfKind($element['index'], ['{']));
}
} else { // 'const', 'property', enum-'case', or 'method' of an interface
$elementEndIndex = $tokens->getNextTokenOfKind($element['index'], [';', '{']);
}
$singleLineElement = true;
for ($i = $element['index'] + 1; $i < $elementEndIndex; ++$i) {
if (str_contains($tokens[$i]->getContent(), "\n")) {
$singleLineElement = false;
break;
}
}
if ($singleLineElement) {
while (true) {
$nextToken = $tokens[$elementEndIndex + 1];
if (($nextToken->isComment() || $nextToken->isWhitespace()) && !str_contains($nextToken->getContent(), "\n")) {
++$elementEndIndex;
} else {
break;
}
}
if ($tokens[$elementEndIndex]->isWhitespace()) {
$elementEndIndex = $tokens->getPrevNonWhitespace($elementEndIndex);
}
}
return $elementEndIndex;
}
}

View File

@@ -0,0 +1,556 @@
<?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\FCT;
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 _ClassReferenceInfo array{start: int, count: int, multiLine: bool}
* @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,
* }
* @phpstan-type _ClassyDefinitionInfo array{
* start: int,
* classy: int,
* open: int,
* extends: false|_ClassReferenceInfo,
* implements: false|_ClassReferenceInfo,
* anonymousClass: bool,
* final: false|int,
* abstract: false|int,
* readonly: false|int,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*/
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 (false !== $classDefInfo['implements']) {
$end = $classDefInfo['implements']['start'];
} elseif (false !== $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 _ClassReferenceInfo $classExtendsInfo
*
* @return _ClassReferenceInfo
*/
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['count']) {
$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 _ClassReferenceInfo $classImplementsInfo
*
* @return _ClassReferenceInfo
*/
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['count']) {
$this->makeClassyDefinitionSingleLine($tokens, $classImplementsInfo['start'], $endIndex);
$classImplementsInfo['multiLine'] = false;
} else {
$this->makeClassyInheritancePartMultiLine($tokens, $classImplementsInfo['start'], $endIndex);
$classImplementsInfo['multiLine'] = true;
}
return $classImplementsInfo;
}
/**
* @param _ClassyDefinitionInfo $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 _ClassyDefinitionInfo
*/
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)) : false;
if (!$tokens[$classyIndex]->isGivenKind(\T_INTERFACE)) {
$implements = $tokens->findGivenKind(\T_IMPLEMENTS, $classyIndex, $openIndex);
$def['implements'] = [] !== $implements ? $this->getClassyInheritanceInfo($tokens, array_key_first($implements)) : 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 _ClassReferenceInfo
*/
private function getClassyInheritanceInfo(Tokens $tokens, int $startIndex): array
{
$implementsInfo = ['start' => $startIndex, 'count' => 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['count'];
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 ($tokens[$i - 1]->isGivenKind(CT::T_ATTRIBUTE_CLOSE) || $tokens[$i + 1]->isGivenKind(FCT::T_ATTRIBUTE)) {
continue;
}
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];
$readonlyToken = clone $tokens[$readonlyIndex];
$accessToken = clone $tokens[$accessModifierIndex];
$tokens[$readonlyIndex] = $accessToken;
$tokens[$accessModifierIndex] = $readonlyToken;
break;
}
}
}

View File

@@ -0,0 +1,67 @@
<?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\AbstractProxyFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
/**
* @author Filippo Tessarotto <zoeslam@gmail.com>
*/
final class FinalClassFixer extends AbstractProxyFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'All classes must be final, except abstract ones and Doctrine entities.',
[
new CodeSample(
'<?php
class MyApp {}
'
),
],
'No exception and no configuration are intentional. Beside Doctrine entities and of course abstract classes, there is no single reason not to declare all classes final. '
.'If you want to subclass a class, mark the parent class as abstract and create two child classes, one empty if necessary: you\'ll gain much more fine grained type-hinting. '
.'If you need to mock a standalone class, create an interface, or maybe it\'s a value-object that shouldn\'t be mocked at all. '
.'If you need to extend a standalone class, create an interface and use the Composite pattern. '
.'If these rules are too strict for you, you can use `FinalInternalClassFixer` instead.',
'Risky when subclassing non-abstract classes.'
);
}
/**
* {@inheritdoc}
*
* Must run before ProtectedToPrivateFixer, SelfStaticAccessorFixer.
*/
public function getPriority(): int
{
return parent::getPriority();
}
protected function createProxyFixers(): array
{
$fixer = new FinalInternalClassFixer();
$fixer->configure([
'include' => [],
'consider_absent_docblock_as_internal_class' => true,
]);
return [$fixer];
}
}

View File

@@ -0,0 +1,370 @@
<?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\ConfigurationException\InvalidFixerConfigurationException;
use PhpCsFixer\DocBlock\DocBlock;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
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\Preg;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
use PhpCsFixer\Utils;
use Symfony\Component\OptionsResolver\Options;
/**
* @phpstan-type _AutogeneratedInputConfiguration array{
* annotation_exclude?: list<string>,
* annotation_include?: list<string>,
* consider_absent_docblock_as_internal_class?: bool,
* exclude?: list<string>,
* include?: list<string>,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* annotation_exclude: array<string, string>,
* annotation_include: array<string, string>,
* consider_absent_docblock_as_internal_class: bool,
* exclude: array<string, string>,
* include: array<string, string>,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class FinalInternalClassFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
private const DEFAULTS = [
'include' => [
'internal',
],
'exclude' => [
'final',
'Entity',
'ORM\Entity',
'ORM\Mapping\Entity',
'Mapping\Entity',
'Document',
'ODM\Document',
],
];
private const CLASS_CANDIDATE_ACCEPT_TYPES = [
CT::T_ATTRIBUTE_CLOSE,
\T_DOC_COMMENT,
\T_COMMENT, // Skip comments
FCT::T_READONLY,
];
private bool $checkAttributes;
public function __construct()
{
parent::__construct();
$this->checkAttributes = \PHP_VERSION_ID >= 8_00_00;
}
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Internal classes should be `final`.',
[
new CodeSample("<?php\n/**\n * @internal\n */\nclass Sample\n{\n}\n"),
new CodeSample(
"<?php\n/**\n * @CUSTOM\n */\nclass A{}\n\n/**\n * @CUSTOM\n * @not-fix\n */\nclass B{}\n",
[
'include' => ['@Custom'],
'exclude' => ['@not-fix'],
]
),
],
null,
'Changing classes to `final` might cause code execution to break.'
);
}
/**
* {@inheritdoc}
*
* Must run before ProtectedToPrivateFixer, SelfStaticAccessorFixer.
* Must run after PhpUnitInternalClassFixer.
*/
public function getPriority(): int
{
return 67;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(\T_CLASS);
}
public function isRisky(): bool
{
return true;
}
protected function configurePostNormalisation(): void
{
$this->assertConfigHasNoConflicts();
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$tokensAnalyzer = new TokensAnalyzer($tokens);
for ($index = $tokens->count() - 1; 0 <= $index; --$index) {
if (!$tokens[$index]->isGivenKind(\T_CLASS) || !$this->isClassCandidate($tokensAnalyzer, $tokens, $index)) {
continue;
}
// make class 'final'
$tokens->insertSlices([
$index => [
new Token([\T_FINAL, 'final']),
new Token([\T_WHITESPACE, ' ']),
],
]);
}
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
$annotationsAsserts = [static function (array $values): bool {
foreach ($values as $value) {
if ('' === $value) {
return false;
}
}
return true;
}];
$annotationsNormalizer = static function (Options $options, array $value): array {
$newValue = [];
foreach ($value as $key) {
if (str_starts_with($key, '@')) {
$key = substr($key, 1);
}
$newValue[strtolower($key)] = true;
}
return $newValue;
};
return new FixerConfigurationResolver([
(new FixerOptionBuilder('annotation_include', 'Class level attribute or annotation tags that must be set in order to fix the class (case insensitive).'))
->setAllowedTypes(['string[]'])
->setAllowedValues($annotationsAsserts)
->setDefault(
array_map(
static fn (string $string) => '@'.$string,
self::DEFAULTS['include'],
),
)
->setNormalizer($annotationsNormalizer)
->setDeprecationMessage('Use `include` to configure PHPDoc annotations tags and attributes.')
->getOption(),
(new FixerOptionBuilder('annotation_exclude', 'Class level attribute or annotation tags that must be omitted to fix the class, even if all of the white list ones are used as well (case insensitive).'))
->setAllowedTypes(['string[]'])
->setAllowedValues($annotationsAsserts)
->setDefault(
array_map(
static fn (string $string) => '@'.$string,
self::DEFAULTS['exclude'],
),
)
->setNormalizer($annotationsNormalizer)
->setDeprecationMessage('Use `exclude` to configure PHPDoc annotations tags and attributes.')
->getOption(),
(new FixerOptionBuilder('include', 'Class level attribute or annotation tags that must be set in order to fix the class (case insensitive).'))
->setAllowedTypes(['string[]'])
->setAllowedValues($annotationsAsserts)
->setDefault(self::DEFAULTS['include'])
->setNormalizer($annotationsNormalizer)
->getOption(),
(new FixerOptionBuilder('exclude', 'Class level attribute or annotation tags that must be omitted to fix the class, even if all of the white list ones are used as well (case insensitive).'))
->setAllowedTypes(['string[]'])
->setAllowedValues($annotationsAsserts)
->setDefault(self::DEFAULTS['exclude'])
->setNormalizer($annotationsNormalizer)
->getOption(),
(new FixerOptionBuilder('consider_absent_docblock_as_internal_class', 'Whether classes without any DocBlock should be fixed to final.'))
->setAllowedTypes(['bool'])
->setDefault(false)
->getOption(),
]);
}
/**
* @param int $index T_CLASS index
*/
private function isClassCandidate(TokensAnalyzer $tokensAnalyzer, Tokens $tokens, int $index): bool
{
if ($tokensAnalyzer->isAnonymousClass($index)) {
return false;
}
$modifiers = $tokensAnalyzer->getClassyModifiers($index);
if (isset($modifiers['final']) || isset($modifiers['abstract'])) {
return false; // ignore class; it is abstract or already final
}
$decisions = [];
$currentIndex = $index;
while (null !== $currentIndex) {
$currentIndex = $tokens->getPrevNonWhitespace($currentIndex);
if (!$tokens[$currentIndex]->isGivenKind(self::CLASS_CANDIDATE_ACCEPT_TYPES)) {
break;
}
if ($this->checkAttributes && $tokens[$currentIndex]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) {
$attributeStartIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_ATTRIBUTE, $currentIndex);
$decisions[] = $this->isClassCandidateBasedOnAttribute($tokens, $attributeStartIndex, $currentIndex);
$currentIndex = $attributeStartIndex;
}
if ($tokens[$currentIndex]->isGivenKind(\T_DOC_COMMENT)) {
$decisions[] = $this->isClassCandidateBasedOnPhpDoc($tokens, $currentIndex);
}
}
if (\in_array(false, $decisions, true)) {
return false;
}
return \in_array(true, $decisions, true)
|| ([] === $decisions && true === $this->configuration['consider_absent_docblock_as_internal_class']);
}
private function isClassCandidateBasedOnPhpDoc(Tokens $tokens, int $index): ?bool
{
$doc = new DocBlock($tokens[$index]->getContent());
$tags = [];
foreach ($doc->getAnnotations() as $annotation) {
if (!Preg::match('/@([^\(\s]+)/', $annotation->getContent(), $matches)) {
continue;
}
$tag = strtolower(substr(array_shift($matches), 1));
$tags[$tag] = true;
}
if (\count(array_intersect_key($this->configuration['exclude'], $tags)) > 0) {
return false;
}
if ($this->isConfiguredAsInclude($tags)) {
return true;
}
return null;
}
private function isClassCandidateBasedOnAttribute(Tokens $tokens, int $startIndex, int $endIndex): ?bool
{
$attributeCandidates = [];
$attributeString = '';
$currentIndex = $startIndex;
while ($currentIndex < $endIndex && null !== ($currentIndex = $tokens->getNextMeaningfulToken($currentIndex))) {
if (!$tokens[$currentIndex]->isGivenKind([\T_STRING, \T_NS_SEPARATOR])) {
if ('' !== $attributeString) {
$attributeCandidates[$attributeString] = true;
$attributeString = '';
}
continue;
}
$attributeString .= strtolower($tokens[$currentIndex]->getContent());
}
if (\count(array_intersect_key($this->configuration['exclude'], $attributeCandidates)) > 0) {
return false;
}
if ($this->isConfiguredAsInclude($attributeCandidates)) {
return true;
}
return null;
}
/**
* @param array<string, bool> $attributes
*/
private function isConfiguredAsInclude(array $attributes): bool
{
if (0 === \count($this->configuration['include'])) {
return true;
}
return \count(array_intersect_key($this->configuration['include'], $attributes)) > 0;
}
private function assertConfigHasNoConflicts(): void
{
foreach (['include' => 'annotation_include', 'exclude' => 'annotation_exclude'] as $newConfigKey => $oldConfigKey) {
$defaults = [];
foreach (self::DEFAULTS[$newConfigKey] as $foo) {
$defaults[strtolower($foo)] = true;
}
$newConfigIsSet = $this->configuration[$newConfigKey] !== $defaults;
$oldConfigIsSet = $this->configuration[$oldConfigKey] !== $defaults;
if ($newConfigIsSet && $oldConfigIsSet) {
throw new InvalidFixerConfigurationException($this->getName(), \sprintf('Configuration cannot contain deprecated option "%s" and new option "%s".', $oldConfigKey, $newConfigKey));
}
if ($oldConfigIsSet) {
$this->configuration[$newConfigKey] = $this->configuration[$oldConfigKey]; // @phpstan-ignore-line crazy mapping, to be removed while cleaning up deprecated options
$this->checkAttributes = false; // run in old mode
}
// if ($newConfigIsSet) - only new config is set, all good
// if (!$newConfigIsSet && !$oldConfigIsSet) - both are set as to default values, all good
unset($this->configuration[$oldConfigKey]); // @phpstan-ignore-line crazy mapping, to be removed while cleaning up deprecated options
}
$intersect = array_intersect_assoc($this->configuration['include'], $this->configuration['exclude']);
if (\count($intersect) > 0) {
throw new InvalidFixerConfigurationException($this->getName(), \sprintf('Annotation cannot be used in both "include" and "exclude" list, got duplicates: %s.', Utils::naturalLanguageJoin(array_keys($intersect))));
}
}
}

View File

@@ -0,0 +1,160 @@
<?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\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Filippo Tessarotto <zoeslam@gmail.com>
*/
final class FinalPublicMethodForAbstractClassFixer extends AbstractFixer
{
/**
* @var array<string, true>
*/
private array $magicMethods = [
'__construct' => true,
'__destruct' => true,
'__call' => true,
'__callstatic' => true,
'__get' => true,
'__set' => true,
'__isset' => true,
'__unset' => true,
'__sleep' => true,
'__wakeup' => true,
'__tostring' => true,
'__invoke' => true,
'__set_state' => true,
'__clone' => true,
'__debuginfo' => true,
];
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'All `public` methods of `abstract` classes should be `final`.',
[
new CodeSample(
'<?php
abstract class AbstractMachine
{
public function start()
{}
}
'
),
],
'Enforce API encapsulation in an inheritance architecture. '
.'If you want to override a method, use the Template method pattern.',
'Risky when overriding `public` methods of `abstract` classes.'
);
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAllTokenKindsFound([\T_ABSTRACT, \T_PUBLIC, \T_FUNCTION]);
}
public function isRisky(): bool
{
return true;
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$abstracts = array_keys($tokens->findGivenKind(\T_ABSTRACT));
foreach (array_reverse($abstracts) as $abstractIndex) {
$classIndex = $tokens->getNextTokenOfKind($abstractIndex, [[\T_CLASS], [\T_FUNCTION]]);
if (!$tokens[$classIndex]->isGivenKind(\T_CLASS)) {
continue;
}
$classOpen = $tokens->getNextTokenOfKind($classIndex, ['{']);
$classClose = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $classOpen);
$this->fixClass($tokens, $classOpen, $classClose);
}
}
private function fixClass(Tokens $tokens, int $classOpenIndex, int $classCloseIndex): void
{
for ($index = $classCloseIndex - 1; $index > $classOpenIndex; --$index) {
// skip method contents
if ($tokens[$index]->equals('}')) {
$index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
continue;
}
// skip non public methods
if (!$tokens[$index]->isGivenKind(\T_PUBLIC)) {
continue;
}
$nextIndex = $tokens->getNextMeaningfulToken($index);
$nextToken = $tokens[$nextIndex];
if ($nextToken->isGivenKind(\T_STATIC)) {
$nextIndex = $tokens->getNextMeaningfulToken($nextIndex);
$nextToken = $tokens[$nextIndex];
}
// skip uses, attributes, constants etc
if (!$nextToken->isGivenKind(\T_FUNCTION)) {
continue;
}
$nextIndex = $tokens->getNextMeaningfulToken($nextIndex);
$nextToken = $tokens[$nextIndex];
// skip magic methods
if (isset($this->magicMethods[strtolower($nextToken->getContent())])) {
continue;
}
$prevIndex = $tokens->getPrevMeaningfulToken($index);
$prevToken = $tokens[$prevIndex];
if ($prevToken->isGivenKind(\T_STATIC)) {
$index = $prevIndex;
$prevIndex = $tokens->getPrevMeaningfulToken($index);
$prevToken = $tokens[$prevIndex];
}
// skip abstract or already final methods
if ($prevToken->isGivenKind([\T_ABSTRACT, \T_FINAL])) {
$index = $prevIndex;
continue;
}
$tokens->insertAt(
$index,
[
new Token([\T_FINAL, 'final']),
new Token([\T_WHITESPACE, ' ']),
]
);
}
}
}

View File

@@ -0,0 +1,93 @@
<?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\WhitespacesAwareFixerInterface;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Ceeram <ceeram@cakephp.org>
*/
final class NoBlankLinesAfterClassOpeningFixer extends AbstractFixer implements WhitespacesAwareFixerInterface
{
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound(Token::getClassyTokenKinds());
}
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'There should be no empty lines after class opening brace.',
[
new CodeSample(
'<?php
final class Sample
{
protected function foo()
{
}
}
'
),
]
);
}
/**
* {@inheritdoc}
*
* Must run after OrderedClassElementsFixer, PhpUnitDataProviderMethodOrderFixer.
*/
public function getPriority(): int
{
return 0;
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
foreach ($tokens as $index => $token) {
if (!$token->isClassy()) {
continue;
}
$startBraceIndex = $tokens->getNextTokenOfKind($index, ['{']);
if (!$tokens[$startBraceIndex + 1]->isWhitespace()) {
continue;
}
$this->fixWhitespace($tokens, $startBraceIndex + 1);
}
}
/**
* Cleanup a whitespace token.
*/
private function fixWhitespace(Tokens $tokens, int $index): void
{
$content = $tokens[$index]->getContent();
// if there is more than one new line in the whitespace, then we need to fix it
if (substr_count($content, "\n") > 1) {
// the final bit of the whitespace must be the next statement's indentation
$tokens[$index] = new Token([\T_WHITESPACE, $this->whitespacesConfig->getLineEnding().substr($content, strrpos($content, "\n") + 1)]);
}
}
}

View File

@@ -0,0 +1,143 @@
<?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\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author ntzm
*/
final class NoNullPropertyInitializationFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Properties MUST not be explicitly initialized with `null` except when they have a type declaration (PHP 7.4).',
[
new CodeSample(
'<?php
class Foo {
public $bar = null;
public ?string $baz = null;
public ?string $baux;
}
'
),
new CodeSample(
'<?php
class Foo {
public static $foo = null;
}
'
),
]
);
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound([\T_CLASS, \T_TRAIT]) && $tokens->isAnyTokenKindsFound([\T_PUBLIC, \T_PROTECTED, \T_PRIVATE, \T_VAR, \T_STATIC]);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$inClass = [];
$classLevel = 0;
for ($index = 0, $count = $tokens->count(); $index < $count; ++$index) {
if ($tokens[$index]->isGivenKind([\T_CLASS, \T_TRAIT])) { // Enums and interfaces do not have properties
++$classLevel;
$inClass[$classLevel] = 1;
$index = $tokens->getNextTokenOfKind($index, ['{']);
continue;
}
if (0 === $classLevel) {
continue;
}
if ($tokens[$index]->equals('{')) {
++$inClass[$classLevel];
continue;
}
if ($tokens[$index]->equals('}')) {
--$inClass[$classLevel];
if (0 === $inClass[$classLevel]) {
unset($inClass[$classLevel]);
--$classLevel;
}
continue;
}
// Ensure we are in a class but not in a method in case there are static variables defined
if (1 !== $inClass[$classLevel]) {
continue;
}
if (!$tokens[$index]->isGivenKind([\T_PUBLIC, \T_PROTECTED, \T_PRIVATE, \T_VAR, \T_STATIC])) {
continue;
}
while (true) {
$varTokenIndex = $index = $tokens->getNextMeaningfulToken($index);
if ($tokens[$index]->isGivenKind(\T_STATIC)) {
$varTokenIndex = $index = $tokens->getNextMeaningfulToken($index);
}
if (!$tokens[$index]->isGivenKind(\T_VARIABLE)) {
break;
}
$index = $tokens->getNextMeaningfulToken($index);
if ($tokens[$index]->equals('=')) {
$index = $tokens->getNextMeaningfulToken($index);
if ($tokens[$index]->isGivenKind(\T_NS_SEPARATOR)) {
$index = $tokens->getNextMeaningfulToken($index);
}
if ($tokens[$index]->equals([\T_STRING, 'null'], false)) {
for ($i = $varTokenIndex + 1; $i <= $index; ++$i) {
if (
!($tokens[$i]->isWhitespace() && str_contains($tokens[$i]->getContent(), "\n"))
&& !$tokens[$i]->isComment()
) {
$tokens->clearAt($i);
}
}
}
++$index;
}
if (!$tokens[$index]->equals(',')) {
break;
}
}
}
}
}

View File

@@ -0,0 +1,408 @@
<?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\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
/**
* @author Matteo Beccati <matteo@beccati.com>
*/
final class NoPhp4ConstructorFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Convert PHP4-style constructors to `__construct`.',
[
new CodeSample('<?php
class Foo
{
public function Foo($bar)
{
}
}
'),
],
null,
'Risky when old style constructor being fixed is overridden or overrides parent one.'
);
}
/**
* {@inheritdoc}
*
* Must run before OrderedClassElementsFixer.
*/
public function getPriority(): int
{
return 75;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(\T_CLASS);
}
public function isRisky(): bool
{
return true;
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$tokensAnalyzer = new TokensAnalyzer($tokens);
$classes = array_keys($tokens->findGivenKind(\T_CLASS));
$numClasses = \count($classes);
for ($i = 0; $i < $numClasses; ++$i) {
$index = $classes[$i];
// is it an anonymous class definition?
if ($tokensAnalyzer->isAnonymousClass($index)) {
continue;
}
// is it inside a namespace?
$nspIndex = $tokens->getPrevTokenOfKind($index, [[\T_NAMESPACE, 'namespace']]);
if (null !== $nspIndex) {
$nspIndex = $tokens->getNextMeaningfulToken($nspIndex);
// make sure it's not the global namespace, as PHP4 constructors are allowed in there
if (!$tokens[$nspIndex]->equals('{')) {
// unless it's the global namespace, the index currently points to the name
$nspIndex = $tokens->getNextTokenOfKind($nspIndex, [';', '{']);
if ($tokens[$nspIndex]->equals(';')) {
// the class is inside a (non-block) namespace, no PHP4-code should be in there
break;
}
// the index points to the { of a block-namespace
$nspEnd = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $nspIndex);
if ($index < $nspEnd) {
// the class is inside a block namespace, skip other classes that might be in it
for ($j = $i + 1; $j < $numClasses; ++$j) {
if ($classes[$j] < $nspEnd) {
++$i;
}
}
// and continue checking the classes that might follow
continue;
}
}
}
$classNameIndex = $tokens->getNextMeaningfulToken($index);
$className = $tokens[$classNameIndex]->getContent();
$classStart = $tokens->getNextTokenOfKind($classNameIndex, ['{']);
$classEnd = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $classStart);
$this->fixConstructor($tokens, $className, $classStart, $classEnd);
$this->fixParent($tokens, $classStart, $classEnd);
}
}
/**
* Fix constructor within a class, if possible.
*
* @param Tokens $tokens the Tokens instance
* @param string $className the class name
* @param int $classStart the class start index
* @param int $classEnd the class end index
*/
private function fixConstructor(Tokens $tokens, string $className, int $classStart, int $classEnd): void
{
$php4 = $this->findFunction($tokens, $className, $classStart, $classEnd);
if (null === $php4) {
return; // no PHP4-constructor!
}
if (isset($php4['modifiers'][\T_ABSTRACT]) || isset($php4['modifiers'][\T_STATIC])) {
return; // PHP4 constructor can't be abstract or static
}
$php5 = $this->findFunction($tokens, '__construct', $classStart, $classEnd);
if (null === $php5) {
// no PHP5-constructor, we can rename the old one to __construct
$tokens[$php4['nameIndex']] = new Token([\T_STRING, '__construct']);
// in some (rare) cases we might have just created an infinite recursion issue
$this->fixInfiniteRecursion($tokens, $php4['bodyIndex'], $php4['endIndex']);
return;
}
// does the PHP4-constructor only call $this->__construct($args, ...)?
[$sequences, $case] = $this->getWrapperMethodSequence($tokens, '__construct', $php4['startIndex'], $php4['bodyIndex']);
foreach ($sequences as $seq) {
if (null !== $tokens->findSequence($seq, $php4['bodyIndex'] - 1, $php4['endIndex'], $case)) {
// good, delete it!
for ($i = $php4['startIndex']; $i <= $php4['endIndex']; ++$i) {
$tokens->clearAt($i);
}
return;
}
}
// does __construct only call the PHP4-constructor (with the same args)?
[$sequences, $case] = $this->getWrapperMethodSequence($tokens, $className, $php4['startIndex'], $php4['bodyIndex']);
foreach ($sequences as $seq) {
if (null !== $tokens->findSequence($seq, $php5['bodyIndex'] - 1, $php5['endIndex'], $case)) {
// that was a weird choice, but we can safely delete it and...
for ($i = $php5['startIndex']; $i <= $php5['endIndex']; ++$i) {
$tokens->clearAt($i);
}
// rename the PHP4 one to __construct
$tokens[$php4['nameIndex']] = new Token([\T_STRING, '__construct']);
return;
}
}
}
/**
* Fix calls to the parent constructor within a class.
*
* @param Tokens $tokens the Tokens instance
* @param int $classStart the class start index
* @param int $classEnd the class end index
*/
private function fixParent(Tokens $tokens, int $classStart, int $classEnd): void
{
// check calls to the parent constructor
foreach ($tokens->findGivenKind(\T_EXTENDS) as $index => $token) {
$parentIndex = $tokens->getNextMeaningfulToken($index);
$parentClass = $tokens[$parentIndex]->getContent();
// using parent::ParentClassName() or ParentClassName::ParentClassName()
$parentSeq = $tokens->findSequence([
[\T_STRING],
[\T_DOUBLE_COLON],
[\T_STRING, $parentClass],
'(',
], $classStart, $classEnd, [2 => false]);
if (null !== $parentSeq) {
// we only need indices
$parentSeq = array_keys($parentSeq);
// match either of the possibilities
if ($tokens[$parentSeq[0]]->equalsAny([[\T_STRING, 'parent'], [\T_STRING, $parentClass]], false)) {
// replace with parent::__construct
$tokens[$parentSeq[0]] = new Token([\T_STRING, 'parent']);
$tokens[$parentSeq[2]] = new Token([\T_STRING, '__construct']);
}
}
foreach (Token::getObjectOperatorKinds() as $objectOperatorKind) {
// using $this->ParentClassName()
$parentSeq = $tokens->findSequence([
[\T_VARIABLE, '$this'],
[$objectOperatorKind],
[\T_STRING, $parentClass],
'(',
], $classStart, $classEnd, [2 => false]);
if (null !== $parentSeq) {
// we only need indices
$parentSeq = array_keys($parentSeq);
// replace call with parent::__construct()
$tokens[$parentSeq[0]] = new Token([
\T_STRING,
'parent',
]);
$tokens[$parentSeq[1]] = new Token([
\T_DOUBLE_COLON,
'::',
]);
$tokens[$parentSeq[2]] = new Token([\T_STRING, '__construct']);
}
}
}
}
/**
* Fix a particular infinite recursion issue happening when the parent class has __construct and the child has only
* a PHP4 constructor that calls the parent constructor as $this->__construct().
*
* @param Tokens $tokens the Tokens instance
* @param int $start the PHP4 constructor body start
* @param int $end the PHP4 constructor body end
*/
private function fixInfiniteRecursion(Tokens $tokens, int $start, int $end): void
{
foreach (Token::getObjectOperatorKinds() as $objectOperatorKind) {
$seq = [
[\T_VARIABLE, '$this'],
[$objectOperatorKind],
[\T_STRING, '__construct'],
];
while (true) {
$callSeq = $tokens->findSequence($seq, $start, $end, [2 => false]);
if (null === $callSeq) {
return;
}
$callSeq = array_keys($callSeq);
$tokens[$callSeq[0]] = new Token([\T_STRING, 'parent']);
$tokens[$callSeq[1]] = new Token([\T_DOUBLE_COLON, '::']);
}
}
}
/**
* Generate the sequence of tokens necessary for the body of a wrapper method that simply
* calls $this->{$method}( [args...] ) with the same arguments as its own signature.
*
* @param Tokens $tokens the Tokens instance
* @param string $method the wrapped method name
* @param int $startIndex function/method start index
* @param int $bodyIndex function/method body index
*
* @return array{list<non-empty-list<array{0: int, 1?: string}|string>>, array{3: false}}
*/
private function getWrapperMethodSequence(Tokens $tokens, string $method, int $startIndex, int $bodyIndex): array
{
$sequences = [];
foreach (Token::getObjectOperatorKinds() as $objectOperatorKind) {
// initialise sequence as { $this->{$method}(
$seq = [
'{',
[\T_VARIABLE, '$this'],
[$objectOperatorKind],
[\T_STRING, $method],
'(',
];
// parse method parameters, if any
$index = $startIndex;
while (true) {
// find the next variable name
$index = $tokens->getNextTokenOfKind($index, [[\T_VARIABLE]]);
if (null === $index || $index >= $bodyIndex) {
// we've reached the body already
break;
}
// append a comma if it's not the first variable
if (\count($seq) > 5) {
$seq[] = ',';
}
// append variable name to the sequence
$seq[] = [\T_VARIABLE, $tokens[$index]->getContent()];
}
// almost done, close the sequence with ); }
$seq[] = ')';
$seq[] = ';';
$seq[] = '}';
$sequences[] = $seq;
}
return [$sequences, [3 => false]];
}
/**
* Find a function or method matching a given name within certain bounds.
*
* Returns:
* - nameIndex (int): The index of the function/method name.
* - startIndex (int): The index of the function/method start.
* - endIndex (int): The index of the function/method end.
* - bodyIndex (int): The index of the function/method body.
* - modifiers (array): The modifiers as array keys and their index as the values, e.g. array(T_PUBLIC => 10)
*
* @param Tokens $tokens the Tokens instance
* @param string $name the function/Method name
* @param int $startIndex the search start index
* @param int $endIndex the search end index
*
* @return null|array{
* nameIndex: int,
* startIndex: int,
* endIndex: int,
* bodyIndex: int,
* modifiers: list<int>,
* }
*/
private function findFunction(Tokens $tokens, string $name, int $startIndex, int $endIndex): ?array
{
$function = $tokens->findSequence([
[\T_FUNCTION],
[\T_STRING, $name],
'(',
], $startIndex, $endIndex, false);
if (null === $function) {
return null;
}
// keep only the indices
$function = array_keys($function);
// find previous block, saving method modifiers for later use
$possibleModifiers = [\T_PUBLIC, \T_PROTECTED, \T_PRIVATE, \T_STATIC, \T_ABSTRACT, \T_FINAL];
$modifiers = [];
$prevBlock = $tokens->getPrevMeaningfulToken($function[0]);
while (null !== $prevBlock && $tokens[$prevBlock]->isGivenKind($possibleModifiers)) {
$modifiers[$tokens[$prevBlock]->getId()] = $prevBlock;
$prevBlock = $tokens->getPrevMeaningfulToken($prevBlock);
}
if (isset($modifiers[\T_ABSTRACT])) {
// abstract methods have no body
$bodyStart = null;
$funcEnd = $tokens->getNextTokenOfKind($function[2], [';']);
} else {
// find method body start and the end of the function definition
$bodyStart = $tokens->getNextTokenOfKind($function[2], ['{']);
$funcEnd = null !== $bodyStart ? $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $bodyStart) : null;
}
return [
'nameIndex' => $function[1],
'startIndex' => $prevBlock + 1,
'endIndex' => $funcEnd,
'bodyIndex' => $bodyStart,
'modifiers' => $modifiers,
];
}
}

View File

@@ -0,0 +1,212 @@
<?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\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\FCT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
/**
* @phpstan-type _AutogeneratedInputConfiguration array{
* private_methods?: bool,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* private_methods: bool,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Filippo Tessarotto <zoeslam@gmail.com>
*/
final class NoUnneededFinalMethodFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Removes `final` from methods where possible.',
[
new CodeSample(
'<?php
final class Foo
{
final public function foo1() {}
final protected function bar() {}
final private function baz() {}
}
class Bar
{
final private function bar1() {}
}
'
),
new CodeSample(
'<?php
final class Foo
{
final private function baz() {}
}
class Bar
{
final private function bar1() {}
}
',
['private_methods' => false]
),
],
null,
'Risky when child class overrides a `private` method.'
);
}
public function isCandidate(Tokens $tokens): bool
{
if (!$tokens->isAllTokenKindsFound([\T_FINAL, \T_FUNCTION])) {
return false;
}
return $tokens->isAnyTokenKindsFound([\T_CLASS, FCT::T_ENUM]);
}
public function isRisky(): bool
{
return true;
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
foreach ($this->getMethods($tokens) as $element) {
$index = $element['method_final_index'];
if ($element['method_of_enum'] || $element['class_is_final']) {
$this->clearFinal($tokens, $index);
continue;
}
if (!$element['method_is_private'] || false === $this->configuration['private_methods'] || $element['method_is_constructor']) {
continue;
}
$this->clearFinal($tokens, $index);
}
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('private_methods', 'Private methods of non-`final` classes must not be declared `final`.'))
->setAllowedTypes(['bool'])
->setDefault(true)
->getOption(),
]);
}
/**
* @return \Generator<array{
* classIndex: int,
* token: Token,
* type: string,
* class_is_final?: bool,
* method_final_index: null|int,
* method_is_constructor?: bool,
* method_is_private: bool,
* method_of_enum: bool
* }>
*/
private function getMethods(Tokens $tokens): \Generator
{
$tokensAnalyzer = new TokensAnalyzer($tokens);
$modifierKinds = [\T_PUBLIC, \T_PROTECTED, \T_PRIVATE, \T_FINAL, \T_ABSTRACT, \T_STATIC];
$enums = [];
$classesAreFinal = [];
foreach ($tokensAnalyzer->getClassyElements() as $index => $element) {
if ('method' !== $element['type']) {
continue; // not a method
}
$classIndex = $element['classIndex'];
if (!\array_key_exists($classIndex, $enums)) {
$enums[$classIndex] = $tokens[$classIndex]->isGivenKind(FCT::T_ENUM);
}
$element['method_final_index'] = null;
$element['method_is_private'] = false;
$previous = $index;
do {
$previous = $tokens->getPrevMeaningfulToken($previous);
if ($tokens[$previous]->isGivenKind(\T_PRIVATE)) {
$element['method_is_private'] = true;
} elseif ($tokens[$previous]->isGivenKind(\T_FINAL)) {
$element['method_final_index'] = $previous;
}
} while ($tokens[$previous]->isGivenKind($modifierKinds));
if ($enums[$classIndex]) {
$element['method_of_enum'] = true;
yield $element;
continue;
}
if (!\array_key_exists($classIndex, $classesAreFinal)) {
$modifiers = $tokensAnalyzer->getClassyModifiers($classIndex);
$classesAreFinal[$classIndex] = isset($modifiers['final']);
}
$element['method_of_enum'] = false;
$element['class_is_final'] = $classesAreFinal[$classIndex];
$element['method_is_constructor'] = '__construct' === strtolower($tokens[$tokens->getNextMeaningfulToken($index)]->getContent());
yield $element;
}
}
private function clearFinal(Tokens $tokens, ?int $index): void
{
if (null === $index) {
return;
}
$tokens->clearAt($index);
++$index;
if ($tokens[$index]->isWhitespace()) {
$tokens->clearAt($index);
}
}
}

View File

@@ -0,0 +1,612 @@
<?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\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\FCT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Utils;
/**
* @phpstan-type _ClassElement array{
* start: int,
* visibility: string,
* abstract: bool,
* static: bool,
* readonly: bool,
* type: string,
* name: string,
* end: int,
* }
* @phpstan-type _AutogeneratedInputConfiguration array{
* case_sensitive?: bool,
* order?: list<string>,
* sort_algorithm?: 'alpha'|'none',
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* case_sensitive: bool,
* order: list<string>,
* sort_algorithm: 'alpha'|'none',
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Gregor Harlan <gharlan@web.de>
*/
final class OrderedClassElementsFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
/** @internal */
public const SORT_ALPHA = 'alpha';
/** @internal */
public const SORT_NONE = 'none';
private const SUPPORTED_SORT_ALGORITHMS = [
self::SORT_NONE,
self::SORT_ALPHA,
];
/**
* @var array<string, null|list<string>> Array containing all class element base types (keys) and their parent types (values)
*/
private const TYPE_HIERARCHY = [
'use_trait' => null,
'public' => null,
'protected' => null,
'private' => null,
'case' => ['public'],
'constant' => null,
'constant_public' => ['constant', 'public'],
'constant_protected' => ['constant', 'protected'],
'constant_private' => ['constant', 'private'],
'property' => null,
'property_static' => ['property'],
'property_public' => ['property', 'public'],
'property_protected' => ['property', 'protected'],
'property_private' => ['property', 'private'],
'property_public_abstract' => ['property_abstract', 'property_public'],
'property_public_readonly' => ['property_readonly', 'property_public'],
'property_protected_abstract' => ['property_abstract', 'property_protected'],
'property_protected_readonly' => ['property_readonly', 'property_protected'],
'property_private_readonly' => ['property_readonly', 'property_private'],
'property_public_static' => ['property_static', 'property_public'],
'property_protected_static' => ['property_static', 'property_protected'],
'property_private_static' => ['property_static', 'property_private'],
'method' => null,
'method_abstract' => ['method'],
'method_static' => ['method'],
'method_public' => ['method', 'public'],
'method_protected' => ['method', 'protected'],
'method_private' => ['method', 'private'],
'method_public_abstract' => ['method_abstract', 'method_public'],
'method_protected_abstract' => ['method_abstract', 'method_protected'],
'method_private_abstract' => ['method_abstract', 'method_private'],
'method_public_abstract_static' => ['method_abstract', 'method_static', 'method_public'],
'method_protected_abstract_static' => ['method_abstract', 'method_static', 'method_protected'],
'method_private_abstract_static' => ['method_abstract', 'method_static', 'method_private'],
'method_public_static' => ['method_static', 'method_public'],
'method_protected_static' => ['method_static', 'method_protected'],
'method_private_static' => ['method_static', 'method_private'],
];
/**
* @var array<string, null> Array containing special method types
*/
private const SPECIAL_TYPES = [
'construct' => null,
'destruct' => null,
'magic' => null,
'phpunit' => null,
];
/**
* @var array<string, int> Resolved configuration array (type => position)
*/
private array $typePosition;
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound(Token::getClassyTokenKinds());
}
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Orders the elements of classes/interfaces/traits/enums.',
[
new CodeSample(
'<?php
final class Example
{
use BarTrait;
use BazTrait;
const C1 = 1;
const C2 = 2;
protected static $protStatProp;
public static $pubStatProp1;
public $pubProp1;
protected $protProp;
var $pubProp2;
private static $privStatProp;
private $privProp;
public static $pubStatProp2;
public $pubProp3;
protected function __construct() {}
private static function privStatFunc() {}
public function pubFunc1() {}
public function __toString() {}
protected function protFunc() {}
function pubFunc2() {}
public static function pubStatFunc1() {}
public function pubFunc3() {}
static function pubStatFunc2() {}
private function privFunc() {}
public static function pubStatFunc3() {}
protected static function protStatFunc() {}
public function __destruct() {}
}
'
),
new CodeSample(
'<?php
class Example
{
public function A(){}
private function B(){}
}
',
['order' => ['method_private', 'method_public']]
),
new CodeSample(
'<?php
class Example
{
public function D(){}
public function B(){}
public function A(){}
public function C(){}
}
',
['order' => ['method_public'], 'sort_algorithm' => self::SORT_ALPHA]
),
new CodeSample(
'<?php
class Example
{
public function Aa(){}
public function AA(){}
public function AwS(){}
public function AWs(){}
}
',
['order' => ['method_public'], 'sort_algorithm' => self::SORT_ALPHA, 'case_sensitive' => true]
),
],
'Accepts a subset of pre-defined element types, special element groups, and custom patterns.
Element types: `[\''.implode('\', \'', array_keys(self::TYPE_HIERARCHY)).'\']`
Special element types: `[\''.implode('\', \'', array_keys(self::SPECIAL_TYPES)).'\']`
Custom values:
- `method:*`: specify a single method name (e.g. `method:__invoke`) to set the order of that specific method.'
);
}
/**
* {@inheritdoc}
*
* Must run before ClassAttributesSeparationFixer, NoBlankLinesAfterClassOpeningFixer, PhpUnitDataProviderMethodOrderFixer, SpaceAfterSemicolonFixer.
* Must run after NoPhp4ConstructorFixer, ProtectedToPrivateFixer.
*/
public function getPriority(): int
{
return 65;
}
protected function configurePostNormalisation(): void
{
$this->typePosition = [];
$position = 0;
foreach ($this->configuration['order'] as $type) {
$this->typePosition[$type] = $position++;
}
foreach (self::TYPE_HIERARCHY as $type => $parents) {
if (isset($this->typePosition[$type])) {
continue;
}
if (null === $parents) {
$this->typePosition[$type] = null;
continue;
}
foreach ($parents as $parent) {
if (isset($this->typePosition[$parent])) {
$this->typePosition[$type] = $this->typePosition[$parent];
continue 2;
}
}
$this->typePosition[$type] = null;
}
$lastPosition = \count($this->configuration['order']);
foreach ($this->typePosition as &$pos) {
if (null === $pos) {
$pos = $lastPosition;
}
$pos *= 10; // last digit is used by phpunit method ordering
}
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
for ($i = 1, $count = $tokens->count(); $i < $count; ++$i) {
if (!$tokens[$i]->isClassy()) {
continue;
}
$i = $tokens->getNextTokenOfKind($i, ['{']);
$elements = $this->getElements($tokens, $i);
if (0 === \count($elements)) {
continue;
}
$endIndex = $elements[array_key_last($elements)]['end'];
$sorted = $this->sortElements($elements);
if ($sorted !== $elements) {
$this->sortTokens($tokens, $i, $endIndex, $sorted);
}
$i = $endIndex;
}
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
$builtIns = array_keys(array_merge(self::TYPE_HIERARCHY, self::SPECIAL_TYPES));
return new FixerConfigurationResolver([
(new FixerOptionBuilder('order', 'List of strings defining order of elements.'))
->setAllowedTypes(['string[]'])
->setAllowedValues([
static function (array $values) use ($builtIns): bool {
foreach ($values as $value) {
if (\in_array($value, $builtIns, true)) {
return true;
}
if ('method:' === substr($value, 0, 7)) {
return true;
}
}
return false;
},
])
->setDefault([
'use_trait',
'case',
'constant_public',
'constant_protected',
'constant_private',
'property_public',
'property_protected',
'property_private',
'construct',
'destruct',
'magic',
'phpunit',
'method_public',
'method_protected',
'method_private',
])
->getOption(),
(new FixerOptionBuilder('sort_algorithm', 'How multiple occurrences of same type statements should be sorted.'))
->setAllowedValues(self::SUPPORTED_SORT_ALGORITHMS)
->setDefault(self::SORT_NONE)
->getOption(),
(new FixerOptionBuilder('case_sensitive', 'Whether the sorting should be case sensitive.'))
->setAllowedTypes(['bool'])
->setDefault(false)
->getOption(),
]);
}
/**
* @return list<_ClassElement>
*/
private function getElements(Tokens $tokens, int $startIndex): array
{
++$startIndex;
$elements = [];
while (true) {
$element = [
'start' => $startIndex,
'visibility' => 'public',
'abstract' => false,
'static' => false,
'readonly' => false,
];
for ($i = $startIndex;; ++$i) {
$token = $tokens[$i];
// class end
if ($token->equals('}')) {
return $elements;
}
if ($token->isGivenKind(\T_ABSTRACT)) {
$element['abstract'] = true;
continue;
}
if ($token->isGivenKind(\T_STATIC)) {
$element['static'] = true;
continue;
}
if ($token->isGivenKind(FCT::T_READONLY)) {
$element['readonly'] = true;
}
if ($token->isGivenKind([\T_PROTECTED, \T_PRIVATE])) {
$element['visibility'] = strtolower($token->getContent());
continue;
}
if (!$token->isGivenKind([CT::T_USE_TRAIT, \T_CASE, \T_CONST, \T_VARIABLE, \T_FUNCTION])) {
continue;
}
$type = $this->detectElementType($tokens, $i);
if (\is_array($type)) {
$element['type'] = $type[0];
$element['name'] = $type[1];
} else {
$element['type'] = $type;
}
if ('property' === $element['type']) {
$element['name'] = $tokens[$i]->getContent();
} elseif ('constant' === $element['type']) {
$equalsSignIndex = $tokens->getNextTokenOfKind($i, ['=']);
$element['name'] = $tokens[$tokens->getPrevMeaningfulToken($equalsSignIndex)]->getContent();
} elseif (\in_array($element['type'], ['use_trait', 'case', 'method', 'magic', 'construct', 'destruct'], true)) {
$element['name'] = $tokens[$tokens->getNextMeaningfulToken($i)]->getContent();
}
$element['end'] = $this->findElementEnd($tokens, $i);
break;
}
$elements[] = $element;
$startIndex = $element['end'] + 1;
}
}
/**
* @return list{string, string}|string type or array of type and name
*/
private function detectElementType(Tokens $tokens, int $index)
{
$token = $tokens[$index];
if ($token->isGivenKind(CT::T_USE_TRAIT)) {
return 'use_trait';
}
if ($token->isGivenKind(\T_CASE)) {
return 'case';
}
if ($token->isGivenKind(\T_CONST)) {
return 'constant';
}
if ($token->isGivenKind(\T_VARIABLE)) {
return 'property';
}
$nameToken = $tokens[$tokens->getNextMeaningfulToken($index)];
if ($nameToken->equals([\T_STRING, '__construct'], false)) {
return 'construct';
}
if ($nameToken->equals([\T_STRING, '__destruct'], false)) {
return 'destruct';
}
if (
$nameToken->equalsAny([
[\T_STRING, 'setUpBeforeClass'],
[\T_STRING, 'doSetUpBeforeClass'],
[\T_STRING, 'tearDownAfterClass'],
[\T_STRING, 'doTearDownAfterClass'],
[\T_STRING, 'setUp'],
[\T_STRING, 'doSetUp'],
[\T_STRING, 'assertPreConditions'],
[\T_STRING, 'assertPostConditions'],
[\T_STRING, 'tearDown'],
[\T_STRING, 'doTearDown'],
], false)
) {
return ['phpunit', strtolower($nameToken->getContent())];
}
return str_starts_with($nameToken->getContent(), '__') ? 'magic' : 'method';
}
private function findElementEnd(Tokens $tokens, int $index): int
{
$index = $tokens->getNextTokenOfKind($index, ['(', '{', ';', [CT::T_PROPERTY_HOOK_BRACE_OPEN]]);
if ($tokens[$index]->equals('(')) {
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
$index = $tokens->getNextTokenOfKind($index, ['{', ';']);
}
if ($tokens[$index]->equals('{')) {
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
}
if ($tokens[$index]->isGivenKind(CT::T_PROPERTY_HOOK_BRACE_OPEN)) {
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PROPERTY_HOOK, $index);
}
for (++$index; $tokens[$index]->isWhitespace(" \t") || $tokens[$index]->isComment(); ++$index);
--$index;
return $tokens[$index]->isWhitespace() ? $index - 1 : $index;
}
/**
* @param list<_ClassElement> $elements
*
* @return list<_ClassElement>
*/
private function sortElements(array $elements): array
{
$getPositionType = function (array $element): int {
$type = $element['type'];
if (\in_array($type, ['method', 'magic', 'phpunit'], true) && isset($this->typePosition["method:{$element['name']}"])) {
return $this->typePosition["method:{$element['name']}"];
}
if (\array_key_exists($type, self::SPECIAL_TYPES)) {
if (isset($this->typePosition[$type])) {
$position = $this->typePosition[$type];
if ('phpunit' === $type) {
$position += [
'setupbeforeclass' => 1,
'dosetupbeforeclass' => 2,
'teardownafterclass' => 3,
'doteardownafterclass' => 4,
'setup' => 5,
'dosetup' => 6,
'assertpreconditions' => 7,
'assertpostconditions' => 8,
'teardown' => 9,
'doteardown' => 10,
][$element['name']];
}
return $position;
}
$type = 'method';
}
if (\in_array($type, ['constant', 'property', 'method'], true)) {
$type .= '_'.$element['visibility'];
if ($element['abstract']) {
$type .= '_abstract';
}
if ($element['static']) {
$type .= '_static';
}
if ($element['readonly']) {
$type .= '_readonly';
}
}
return $this->typePosition[$type];
};
return Utils::stableSort(
$elements,
/**
* @return array{element: _ClassElement, position: int}
*/
static fn (array $element): array => ['element' => $element, 'position' => $getPositionType($element)],
/**
* @param array{element: _ClassElement, position: int} $a
* @param array{element: _ClassElement, position: int} $b
*
* @return -1|0|1
*/
fn (array $a, array $b): int => ($a['position'] === $b['position']) ? $this->sortGroupElements($a['element'], $b['element']) : $a['position'] <=> $b['position'],
);
}
/**
* @param _ClassElement $a
* @param _ClassElement $b
*/
private function sortGroupElements(array $a, array $b): int
{
if (self::SORT_ALPHA === $this->configuration['sort_algorithm']) {
return true === $this->configuration['case_sensitive']
? $a['name'] <=> $b['name']
: strcasecmp($a['name'], $b['name']);
}
return $a['start'] <=> $b['start'];
}
/**
* @param list<_ClassElement> $elements
*/
private function sortTokens(Tokens $tokens, int $startIndex, int $endIndex, array $elements): void
{
$replaceTokens = [];
foreach ($elements as $element) {
for ($i = $element['start']; $i <= $element['end']; ++$i) {
$replaceTokens[] = clone $tokens[$i];
}
}
$tokens->overrideRange($startIndex + 1, $endIndex, $replaceTokens);
}
}

View File

@@ -0,0 +1,273 @@
<?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\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\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @phpstan-type _AutogeneratedInputConfiguration array{
* case_sensitive?: bool,
* direction?: 'ascend'|'descend',
* order?: 'alpha'|'length',
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* case_sensitive: bool,
* direction: 'ascend'|'descend',
* order: 'alpha'|'length',
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Dave van der Brugge <dmvdbrugge@gmail.com>
*/
final class OrderedInterfacesFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
/** @internal */
public const OPTION_DIRECTION = 'direction';
/** @internal */
public const OPTION_ORDER = 'order';
/** @internal */
public const DIRECTION_ASCEND = 'ascend';
/** @internal */
public const DIRECTION_DESCEND = 'descend';
/** @internal */
public const ORDER_ALPHA = 'alpha';
/** @internal */
public const ORDER_LENGTH = 'length';
/**
* Array of supported directions in configuration.
*
* @var list<string>
*/
private const SUPPORTED_DIRECTION_OPTIONS = [
self::DIRECTION_ASCEND,
self::DIRECTION_DESCEND,
];
/**
* Array of supported orders in configuration.
*
* @var list<string>
*/
private const SUPPORTED_ORDER_OPTIONS = [
self::ORDER_ALPHA,
self::ORDER_LENGTH,
];
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Orders the interfaces in an `implements` or `interface extends` clause.',
[
new CodeSample(
"<?php\n\nfinal class ExampleA implements Gamma, Alpha, Beta {}\n\ninterface ExampleB extends Gamma, Alpha, Beta {}\n"
),
new CodeSample(
"<?php\n\nfinal class ExampleA implements Gamma, Alpha, Beta {}\n\ninterface ExampleB extends Gamma, Alpha, Beta {}\n",
[self::OPTION_DIRECTION => self::DIRECTION_DESCEND]
),
new CodeSample(
"<?php\n\nfinal class ExampleA implements MuchLonger, Short, Longer {}\n\ninterface ExampleB extends MuchLonger, Short, Longer {}\n",
[self::OPTION_ORDER => self::ORDER_LENGTH]
),
new CodeSample(
"<?php\n\nfinal class ExampleA implements MuchLonger, Short, Longer {}\n\ninterface ExampleB extends MuchLonger, Short, Longer {}\n",
[
self::OPTION_ORDER => self::ORDER_LENGTH,
self::OPTION_DIRECTION => self::DIRECTION_DESCEND,
]
),
new CodeSample(
"<?php\n\nfinal class ExampleA implements IgnorecaseB, IgNoReCaSeA, IgnoreCaseC {}\n\ninterface ExampleB extends IgnorecaseB, IgNoReCaSeA, IgnoreCaseC {}\n",
[
self::OPTION_ORDER => self::ORDER_ALPHA,
]
),
new CodeSample(
"<?php\n\nfinal class ExampleA implements Casesensitivea, CaseSensitiveA, CasesensitiveA {}\n\ninterface ExampleB extends Casesensitivea, CaseSensitiveA, CasesensitiveA {}\n",
[
self::OPTION_ORDER => self::ORDER_ALPHA,
'case_sensitive' => true,
]
),
],
);
}
/**
* {@inheritdoc}
*
* Must run after FullyQualifiedStrictTypesFixer.
*/
public function getPriority(): int
{
return 0;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(\T_IMPLEMENTS)
|| $tokens->isAllTokenKindsFound([\T_INTERFACE, \T_EXTENDS]);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
foreach ($tokens as $index => $token) {
if (!$token->isGivenKind(\T_IMPLEMENTS)) {
if (!$token->isGivenKind(\T_EXTENDS)) {
continue;
}
$nameTokenIndex = $tokens->getPrevMeaningfulToken($index);
$interfaceTokenIndex = $tokens->getPrevMeaningfulToken($nameTokenIndex);
$interfaceToken = $tokens[$interfaceTokenIndex];
if (!$interfaceToken->isGivenKind(\T_INTERFACE)) {
continue;
}
}
$implementsStart = $index + 1;
$implementsEnd = $tokens->getPrevMeaningfulToken($tokens->getNextTokenOfKind($implementsStart, ['{']));
$interfacesTokens = $this->getInterfaces($tokens, $implementsStart, $implementsEnd);
if (1 === \count($interfacesTokens)) {
continue;
}
$interfaces = [];
foreach ($interfacesTokens as $interfaceIndex => $interface) {
$interfaceTokens = Tokens::fromArray($interface);
$normalized = '';
$actualInterfaceIndex = $interfaceTokens->getNextMeaningfulToken(-1);
while ($interfaceTokens->offsetExists($actualInterfaceIndex)) {
$token = $interfaceTokens[$actualInterfaceIndex];
if ($token->isComment() || $token->isWhitespace()) {
break;
}
$normalized .= str_replace('\\', ' ', $token->getContent());
++$actualInterfaceIndex;
}
$interfaces[$interfaceIndex] = [
'tokens' => $interface,
'normalized' => $normalized,
'originalIndex' => $interfaceIndex,
];
}
usort($interfaces, function (array $first, array $second): int {
$score = self::ORDER_LENGTH === $this->configuration[self::OPTION_ORDER]
? \strlen($first['normalized']) - \strlen($second['normalized'])
: (
true === $this->configuration['case_sensitive']
? $first['normalized'] <=> $second['normalized']
: strcasecmp($first['normalized'], $second['normalized'])
);
if (self::DIRECTION_DESCEND === $this->configuration[self::OPTION_DIRECTION]) {
$score *= -1;
}
return $score;
});
$changed = false;
foreach ($interfaces as $interfaceIndex => $interface) {
if ($interface['originalIndex'] !== $interfaceIndex) {
$changed = true;
break;
}
}
if (!$changed) {
continue;
}
$newTokens = array_shift($interfaces)['tokens'];
foreach ($interfaces as $interface) {
array_push($newTokens, new Token(','), ...$interface['tokens']);
}
$tokens->overrideRange($implementsStart, $implementsEnd, $newTokens);
}
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder(self::OPTION_ORDER, 'How the interfaces should be ordered.'))
->setAllowedValues(self::SUPPORTED_ORDER_OPTIONS)
->setDefault(self::ORDER_ALPHA)
->getOption(),
(new FixerOptionBuilder(self::OPTION_DIRECTION, 'Which direction the interfaces should be ordered.'))
->setAllowedValues(self::SUPPORTED_DIRECTION_OPTIONS)
->setDefault(self::DIRECTION_ASCEND)
->getOption(),
(new FixerOptionBuilder('case_sensitive', 'Whether the sorting should be case sensitive.'))
->setAllowedTypes(['bool'])
->setDefault(false)
->getOption(),
]);
}
/**
* @return array<int, list<Token>>
*/
private function getInterfaces(Tokens $tokens, int $implementsStart, int $implementsEnd): array
{
$interfaces = [];
$interfaceIndex = 0;
for ($i = $implementsStart; $i <= $implementsEnd; ++$i) {
if ($tokens[$i]->equals(',')) {
++$interfaceIndex;
$interfaces[$interfaceIndex] = [];
continue;
}
$interfaces[$interfaceIndex][] = $tokens[$i];
}
return $interfaces;
}
}

View File

@@ -0,0 +1,226 @@
<?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\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\Tokens;
/**
* @phpstan-type _AutogeneratedInputConfiguration array{
* case_sensitive?: bool,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* case_sensitive: bool,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*/
final class OrderedTraitsFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Trait `use` statements must be sorted alphabetically.',
[
new CodeSample("<?php class Foo { \nuse Z; use A; }\n"),
new CodeSample(
"<?php class Foo { \nuse Aaa; use AA; }\n",
[
'case_sensitive' => true,
]
),
],
null,
'Risky when depending on order of the imports.'
);
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(CT::T_USE_TRAIT);
}
public function isRisky(): bool
{
return true;
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('case_sensitive', 'Whether the sorting should be case sensitive.'))
->setAllowedTypes(['bool'])
->setDefault(false)
->getOption(),
]);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
foreach ($this->findUseStatementsGroups($tokens) as $uses) {
$this->sortUseStatements($tokens, $uses);
}
}
/**
* @return iterable<array<int, Tokens>>
*/
private function findUseStatementsGroups(Tokens $tokens): iterable
{
$uses = [];
for ($index = 1, $max = \count($tokens); $index < $max; ++$index) {
$token = $tokens[$index];
if ($token->isWhitespace() || $token->isComment()) {
continue;
}
if (!$token->isGivenKind(CT::T_USE_TRAIT)) {
if (\count($uses) > 0) {
yield $uses;
$uses = [];
}
continue;
}
$startIndex = $tokens->getNextNonWhitespace($tokens->getPrevMeaningfulToken($index));
$endIndex = $tokens->getNextTokenOfKind($index, [';', '{']);
if ($tokens[$endIndex]->equals('{')) {
$endIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $endIndex);
}
$use = [];
for ($i = $startIndex; $i <= $endIndex; ++$i) {
$use[] = $tokens[$i];
}
$uses[$startIndex] = Tokens::fromArray($use);
$index = $endIndex;
}
}
/**
* @param array<int, Tokens> $uses
*/
private function sortUseStatements(Tokens $tokens, array $uses): void
{
foreach ($uses as $use) {
$this->sortMultipleTraitsInStatement($use);
}
$this->sort($tokens, $uses);
}
private function sortMultipleTraitsInStatement(Tokens $use): void
{
$traits = [];
$indexOfName = null;
$name = [];
for ($index = 0, $max = \count($use); $index < $max; ++$index) {
$token = $use[$index];
if ($token->isGivenKind([\T_STRING, \T_NS_SEPARATOR])) {
$name[] = $token;
if (null === $indexOfName) {
$indexOfName = $index;
}
continue;
}
if ($token->equalsAny([',', ';', '{'])) {
$traits[$indexOfName] = Tokens::fromArray($name);
$name = [];
$indexOfName = null;
}
if ($token->equals('{')) {
$index = $use->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
}
}
$this->sort($use, $traits);
}
/**
* @param array<int, Tokens> $elements
*/
private function sort(Tokens $tokens, array $elements): void
{
$toTraitName = static function (Tokens $use): string {
$string = '';
foreach ($use as $token) {
if ($token->equalsAny([';', '{'])) {
break;
}
if ($token->isGivenKind([\T_NS_SEPARATOR, \T_STRING])) {
$string .= $token->getContent();
}
}
return ltrim($string, '\\');
};
$sortedElements = $elements;
uasort(
$sortedElements,
fn (Tokens $useA, Tokens $useB): int => true === $this->configuration['case_sensitive']
? $toTraitName($useA) <=> $toTraitName($useB)
: strcasecmp($toTraitName($useA), $toTraitName($useB))
);
$sortedElements = array_combine(
array_keys($elements),
array_values($sortedElements)
);
$beforeOverrideCount = $tokens->count();
foreach (array_reverse($sortedElements, true) as $index => $tokensToInsert) {
$tokens->overrideRange(
$index,
$index + \count($elements[$index]) - 1,
$tokensToInsert
);
}
if ($beforeOverrideCount < $tokens->count()) {
$tokens->clearEmptyTokens();
}
}
}

View File

@@ -0,0 +1,448 @@
<?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\FixerConfiguration\FixerConfigurationResolver;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\FixerDefinition\VersionSpecification;
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\TypeAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
/**
* @phpstan-type _AutogeneratedInputConfiguration array{
* case_sensitive?: bool,
* null_adjustment?: 'always_first'|'always_last'|'none',
* sort_algorithm?: 'alpha'|'none',
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* case_sensitive: bool,
* null_adjustment: 'always_first'|'always_last'|'none',
* sort_algorithm: 'alpha'|'none',
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author John Paul E. Balandan, CPA <paulbalandan@gmail.com>
*/
final class OrderedTypesFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
private const PROPERTY_MODIFIERS = [\T_PRIVATE, \T_PROTECTED, \T_PUBLIC, \T_STATIC, \T_VAR, FCT::T_READONLY, FCT::T_PRIVATE_SET, FCT::T_PROTECTED_SET, FCT::T_PUBLIC_SET];
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Sort union types and intersection types using configured order.',
[
new CodeSample(
'<?php
try {
cache()->save($foo);
} catch (\RuntimeException|CacheException $e) {
logger($e);
throw $e;
}
'
),
new VersionSpecificCodeSample(
'<?php
interface Foo
{
public function bar(\Aaa|\AA $foo): string|int;
}
',
new VersionSpecification(8_00_00),
[
'case_sensitive' => true,
]
),
new VersionSpecificCodeSample(
'<?php
interface Foo
{
public function bar(null|string|int $foo): string|int;
public function foo(\Stringable&\Countable $obj): int;
}
',
new VersionSpecification(8_01_00),
['null_adjustment' => 'always_last']
),
new VersionSpecificCodeSample(
'<?php
interface Bar
{
public function bar(null|string|int $foo): string|int;
}
',
new VersionSpecification(8_00_00),
[
'sort_algorithm' => 'none',
'null_adjustment' => 'always_last',
]
),
]
);
}
/**
* {@inheritdoc}
*
* Must run before TypesSpacesFixer.
* Must run after NullableTypeDeclarationFixer, NullableTypeDeclarationForDefaultNullValueFixer.
*/
public function getPriority(): int
{
return 0;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound([CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION]);
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('sort_algorithm', 'Whether the types should be sorted alphabetically, or not sorted.'))
->setAllowedValues(['alpha', 'none'])
->setDefault('alpha')
->getOption(),
(new FixerOptionBuilder('null_adjustment', 'Forces the position of `null` (overrides `sort_algorithm`).'))
->setAllowedValues(['always_first', 'always_last', 'none'])
->setDefault('always_first') // @TODO 4.0 change to 'always_last', as recommended in https://github.com/php-fig/per-coding-style/blob/master/migration-3.0.md#section-25---keywords-and-types
->getOption(),
(new FixerOptionBuilder('case_sensitive', 'Whether the sorting should be case sensitive.'))
->setAllowedTypes(['bool'])
->setDefault(false)
->getOption(),
]);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$functionsAnalyzer = new FunctionsAnalyzer();
foreach ($this->getElements($tokens) as $index => $type) {
if ('catch' === $type) {
$this->fixCatchArgumentType($tokens, $index);
continue;
}
if ('property' === $type) {
$this->fixPropertyType($tokens, $index);
continue;
}
$this->fixMethodArgumentType($functionsAnalyzer, $tokens, $index);
$this->fixMethodReturnType($functionsAnalyzer, $tokens, $index);
}
}
/**
* @return array<int, string>
*
* @phpstan-return array<int, 'catch'|'method'|'property'>
*/
private function getElements(Tokens $tokens): array
{
$tokensAnalyzer = new TokensAnalyzer($tokens);
$elements = array_map(
static fn (array $element): string => $element['type'],
array_filter(
$tokensAnalyzer->getClassyElements(),
static fn (array $element): bool => \in_array($element['type'], ['method', 'property'], true)
)
);
foreach ($tokens as $index => $token) {
if ($token->isGivenKind(\T_CATCH)) {
$elements[$index] = 'catch';
continue;
}
if (
$token->isGivenKind(\T_FN)
|| ($token->isGivenKind(\T_FUNCTION) && !isset($elements[$index]))
) {
$elements[$index] = 'method';
}
}
return $elements;
}
private function collectTypeAnalysis(Tokens $tokens, int $startIndex, int $endIndex): ?TypeAnalysis
{
$type = '';
$typeStartIndex = $tokens->getNextMeaningfulToken($startIndex);
$typeEndIndex = $typeStartIndex;
for ($i = $typeStartIndex; $i < $endIndex; ++$i) {
if ($tokens[$i]->isWhitespace() || $tokens[$i]->isComment()) {
continue;
}
$type .= $tokens[$i]->getContent();
$typeEndIndex = $i;
}
return '' !== $type ? new TypeAnalysis($type, $typeStartIndex, $typeEndIndex) : null;
}
private function fixCatchArgumentType(Tokens $tokens, int $index): void
{
$catchStart = $tokens->getNextTokenOfKind($index, ['(']);
$catchEnd = $tokens->getNextTokenOfKind($catchStart, [')', [\T_VARIABLE]]);
$catchArgumentType = $this->collectTypeAnalysis($tokens, $catchStart, $catchEnd);
if (null === $catchArgumentType || !$this->isTypeSortable($catchArgumentType)) {
return; // nothing to fix
}
$this->sortTypes($catchArgumentType, $tokens);
}
private function fixPropertyType(Tokens $tokens, int $index): void
{
$propertyIndex = $index;
do {
$index = $tokens->getPrevMeaningfulToken($index);
} while (!$tokens[$index]->isGivenKind(self::PROPERTY_MODIFIERS));
$propertyType = $this->collectTypeAnalysis($tokens, $index, $propertyIndex);
if (null === $propertyType || !$this->isTypeSortable($propertyType)) {
return; // nothing to fix
}
$this->sortTypes($propertyType, $tokens);
}
private function fixMethodArgumentType(FunctionsAnalyzer $functionsAnalyzer, Tokens $tokens, int $index): void
{
foreach ($functionsAnalyzer->getFunctionArguments($tokens, $index) as $argumentInfo) {
$argumentType = $argumentInfo->getTypeAnalysis();
if (null === $argumentType || !$this->isTypeSortable($argumentType)) {
continue; // nothing to fix
}
$this->sortTypes($argumentType, $tokens);
}
}
private function fixMethodReturnType(FunctionsAnalyzer $functionsAnalyzer, Tokens $tokens, int $index): void
{
$returnType = $functionsAnalyzer->getFunctionReturnType($tokens, $index);
if (null === $returnType || !$this->isTypeSortable($returnType)) {
return; // nothing to fix
}
$this->sortTypes($returnType, $tokens);
}
private function sortTypes(TypeAnalysis $typeAnalysis, Tokens $tokens): void
{
$type = $typeAnalysis->getName();
if (str_contains($type, '|') && str_contains($type, '&')) {
// a DNF type of the form (A&B)|C, available as of PHP 8.2
[$originalTypes, $glue] = $this->collectDisjunctiveNormalFormTypes($type);
} else {
[$originalTypes, $glue] = $this->collectUnionOrIntersectionTypes($type);
}
// If the $types array is coming from a DNF type, then we have parts
// which are also array. If so, we sort those sub-types first before
// running the sorting algorithm to the entire $types array.
$sortedTypes = array_map(function ($subType) {
if (\is_array($subType)) {
return $this->runTypesThroughSortingAlgorithm($subType);
}
return $subType;
}, $originalTypes);
$sortedTypes = $this->runTypesThroughSortingAlgorithm($sortedTypes);
if ($sortedTypes === $originalTypes) {
return;
}
$tokens->overrideRange(
$typeAnalysis->getStartIndex(),
$typeAnalysis->getEndIndex(),
$this->createTypeDeclarationTokens($sortedTypes, $glue)
);
}
private function isTypeSortable(TypeAnalysis $type): bool
{
return str_contains($type->getName(), '|') || str_contains($type->getName(), '&');
}
/**
* @return array{0: list<list<string>|string>, 1: string}
*/
private function collectDisjunctiveNormalFormTypes(string $type): array
{
$types = array_map(static function (string $subType) {
if (str_starts_with($subType, '(')) {
return explode('&', trim($subType, '()'));
}
return $subType;
}, explode('|', $type));
return [$types, '|'];
}
/**
* @return array{0: list<string>, 1: string}
*/
private function collectUnionOrIntersectionTypes(string $type): array
{
$types = explode('|', $type);
$glue = '|';
if (1 === \count($types)) {
$types = explode('&', $type);
$glue = '&';
}
return [$types, $glue];
}
/**
* @param list<list<string>|string> $types
*
* @return ($types is list<string> ? list<string> : list<list<string>>)
*/
private function runTypesThroughSortingAlgorithm(array $types): array
{
$normalizeType = static fn (string $type): string => Preg::replace('/^\\\?/', '', $type);
usort($types, function ($a, $b) use ($normalizeType): int {
if (\is_array($a)) {
$a = implode('&', $a);
}
if (\is_array($b)) {
$b = implode('&', $b);
}
$a = $normalizeType($a);
$b = $normalizeType($b);
$lowerCaseA = strtolower($a);
$lowerCaseB = strtolower($b);
if ('none' !== $this->configuration['null_adjustment']) {
if ('null' === $lowerCaseA && 'null' !== $lowerCaseB) {
return 'always_last' === $this->configuration['null_adjustment'] ? 1 : -1;
}
if ('null' !== $lowerCaseA && 'null' === $lowerCaseB) {
return 'always_last' === $this->configuration['null_adjustment'] ? -1 : 1;
}
}
if ('alpha' === $this->configuration['sort_algorithm']) {
return true === $this->configuration['case_sensitive'] ? $a <=> $b : strcasecmp($a, $b);
}
return 0;
});
return $types;
}
/**
* @param list<list<string>|string> $types
*
* @return list<Token>
*/
private function createTypeDeclarationTokens(array $types, string $glue, bool $isDisjunctive = false): array
{
$specialTypes = [
'array' => [CT::T_ARRAY_TYPEHINT, 'array'],
'callable' => [\T_CALLABLE, 'callable'],
'static' => [\T_STATIC, 'static'],
];
$count = \count($types);
$newTokens = [];
foreach ($types as $i => $type) {
if (\is_array($type)) {
$newTokens = [
...$newTokens,
...$this->createTypeDeclarationTokens($type, '&', true),
];
} elseif (isset($specialTypes[$type])) {
$newTokens[] = new Token($specialTypes[$type]);
} else {
foreach (explode('\\', $type) as $nsIndex => $value) {
if (0 === $nsIndex && '' === $value) {
continue;
}
if ($nsIndex > 0) {
$newTokens[] = new Token([\T_NS_SEPARATOR, '\\']);
}
$newTokens[] = new Token([\T_STRING, $value]);
}
}
if ($i <= $count - 2) {
$newTokens[] = new Token([
'|' => [CT::T_TYPE_ALTERNATION, '|'],
'&' => [CT::T_TYPE_INTERSECTION, '&'],
][$glue]);
}
}
if ($isDisjunctive) {
array_unshift($newTokens, new Token([CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_OPEN, '(']));
$newTokens[] = new Token([CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_CLOSE, ')']);
}
return $newTokens;
}
}

View File

@@ -0,0 +1,129 @@
<?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\DocBlock\DocBlock;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\FixerDefinition\VersionSpecification;
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Marcel Behrmann <marcel@behrmann.dev>
*/
final class PhpdocReadonlyClassCommentToKeywordFixer extends AbstractFixer
{
/**
* {@inheritdoc}
*
* Must run before NoEmptyPhpdocFixer, NoExtraBlankLinesFixer, PhpdocAlignFixer.
* Must run after AlignMultilineCommentFixer, CommentToPhpdocFixer, PhpdocIndentFixer, PhpdocScalarFixer, PhpdocToCommentFixer, PhpdocTypesFixer.
*/
public function getPriority(): int
{
return 4;
}
public function isCandidate(Tokens $tokens): bool
{
return \PHP_VERSION_ID >= 8_02_00 && $tokens->isTokenKindFound(\T_DOC_COMMENT);
}
public function isRisky(): bool
{
return true;
}
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Converts readonly comment on classes to the readonly keyword.',
[
new VersionSpecificCodeSample(
<<<EOT
<?php
/** @readonly */
class C {
}\n
EOT,
new VersionSpecification(8_02_00)
),
],
null,
'If classes marked with `@readonly` annotation were extended anyway, applying this fixer may break the inheritance for their child classes.'
);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
foreach ($tokens as $index => $token) {
if (!$token->isGivenKind(\T_DOC_COMMENT)) {
continue;
}
$doc = new DocBlock($token->getContent());
$annotations = $doc->getAnnotationsOfType('readonly');
if (0 === \count($annotations)) {
continue;
}
foreach ($annotations as $annotation) {
$annotation->remove();
}
$mainIndex = $index;
$index = $tokens->getNextMeaningfulToken($index);
$addReadonly = true;
while ($tokens[$index]->isGivenKind([
\T_ABSTRACT,
\T_FINAL,
\T_PRIVATE,
\T_PUBLIC,
\T_PROTECTED,
\T_READONLY,
])) {
if ($tokens[$index]->isGivenKind(\T_READONLY)) {
$addReadonly = false;
}
$index = $tokens->getNextMeaningfulToken($index);
}
if (!$tokens[$index]->isGivenKind(\T_CLASS)) {
continue;
}
if ($addReadonly) {
$tokens->insertAt($index, [new Token([\T_READONLY, 'readonly']), new Token([\T_WHITESPACE, ' '])]);
}
$newContent = $doc->getContent();
if ('' === $newContent) {
$tokens->clearTokenAndMergeSurroundingWhitespace($mainIndex);
continue;
}
$tokens[$mainIndex] = new Token([\T_DOC_COMMENT, $doc->getContent()]);
}
}
}

View File

@@ -0,0 +1,174 @@
<?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\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
/**
* @author Filippo Tessarotto <zoeslam@gmail.com>
*/
final class ProtectedToPrivateFixer extends AbstractFixer
{
private const MODIFIER_KINDS = [\T_PUBLIC, \T_PROTECTED, \T_PRIVATE, \T_FINAL, \T_ABSTRACT, \T_NS_SEPARATOR, \T_STRING, CT::T_NULLABLE_TYPE, CT::T_ARRAY_TYPEHINT, \T_STATIC, CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION, FCT::T_READONLY, FCT::T_PRIVATE_SET, FCT::T_PROTECTED_SET];
private TokensAnalyzer $tokensAnalyzer;
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Converts `protected` variables and methods to `private` where possible.',
[
new CodeSample(
'<?php
final class Sample
{
protected $a;
protected function test()
{
}
}
'
),
]
);
}
/**
* {@inheritdoc}
*
* Must run before OrderedClassElementsFixer, StaticPrivateMethodFixer.
* Must run after FinalClassFixer, FinalInternalClassFixer.
*/
public function getPriority(): int
{
return 66;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound([\T_PROTECTED, CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED, FCT::T_PROTECTED_SET])
&& (
$tokens->isAllTokenKindsFound([\T_CLASS, \T_FINAL])
|| $tokens->isTokenKindFound(FCT::T_ENUM)
);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$this->tokensAnalyzer = new TokensAnalyzer($tokens);
$classesCandidate = [];
$classElementTypes = ['method' => true, 'property' => true, 'promoted_property' => true, 'const' => true];
foreach ($this->tokensAnalyzer->getClassyElements() as $index => $element) {
$classIndex = $element['classIndex'];
$classesCandidate[$classIndex] ??= $this->isClassCandidate($tokens, $classIndex);
if (false === $classesCandidate[$classIndex]) {
continue;
}
if (!isset($classElementTypes[$element['type']])) {
continue;
}
$previousIndex = $index;
$protectedIndex = null;
$protectedPromotedIndex = null;
$protectedSetIndex = null;
$isFinal = false;
do {
$previousIndex = $tokens->getPrevMeaningfulToken($previousIndex);
if ($tokens[$previousIndex]->isGivenKind(\T_PROTECTED)) {
$protectedIndex = $previousIndex;
} elseif ($tokens[$previousIndex]->isGivenKind(CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED)) {
$protectedPromotedIndex = $previousIndex;
} elseif ($tokens[$previousIndex]->isGivenKind(FCT::T_PROTECTED_SET)) {
$protectedSetIndex = $previousIndex;
} elseif ($tokens[$previousIndex]->isGivenKind(\T_FINAL)) {
$isFinal = true;
}
} while ($tokens[$previousIndex]->isGivenKind(self::MODIFIER_KINDS));
if ($isFinal && 'const' === $element['type']) {
continue; // Final constants cannot be private
}
if (null !== $protectedIndex) {
$tokens[$protectedIndex] = new Token([\T_PRIVATE, 'private']);
}
if (null !== $protectedPromotedIndex) {
$tokens[$protectedPromotedIndex] = new Token([CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE, 'private']);
}
if (null !== $protectedSetIndex) {
$tokens[$protectedSetIndex] = new Token([\T_PRIVATE_SET, 'private(set)']);
}
}
}
/**
* Consider symbol as candidate for fixing if it's:
* - an Enum (PHP8.1+)
* - a class, which:
* - is not anonymous
* - is final
* - does not use traits
* - does not extend other class.
*/
private function isClassCandidate(Tokens $tokens, int $classIndex): bool
{
if ($tokens[$classIndex]->isGivenKind(FCT::T_ENUM)) {
return true;
}
if (!$tokens[$classIndex]->isGivenKind(\T_CLASS) || $this->tokensAnalyzer->isAnonymousClass($classIndex)) {
return false;
}
$modifiers = $this->tokensAnalyzer->getClassyModifiers($classIndex);
if (!isset($modifiers['final'])) {
return false;
}
$classNameIndex = $tokens->getNextMeaningfulToken($classIndex); // move to class name as anonymous class is never "final"
$classExtendsIndex = $tokens->getNextMeaningfulToken($classNameIndex); // move to possible "extends"
if ($tokens[$classExtendsIndex]->isGivenKind(\T_EXTENDS)) {
return false;
}
if (!$tokens->isTokenKindFound(CT::T_USE_TRAIT)) {
return true; // cheap test
}
$classOpenIndex = $tokens->getNextTokenOfKind($classNameIndex, ['{']);
$classCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $classOpenIndex);
$useIndex = $tokens->getNextTokenOfKind($classOpenIndex, [[CT::T_USE_TRAIT]]);
return null === $useIndex || $useIndex > $classCloseIndex;
}
}

View File

@@ -0,0 +1,200 @@
<?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\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
/**
* @author Gregor Harlan <gharlan@web.de>
*/
final class SelfAccessorFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Inside class or interface element `self` should be preferred to the class name itself.',
[
new CodeSample(
'<?php
class Sample
{
const BAZ = 1;
const BAR = Sample::BAZ;
public function getBar()
{
return Sample::BAR;
}
}
'
),
],
null,
'Risky when using dynamic calls like get_called_class() or late static binding.'
);
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound([\T_CLASS, \T_INTERFACE]);
}
/**
* {@inheritdoc}
*
* Must run after PsrAutoloadingFixer.
*/
public function getPriority(): int
{
return -11;
}
public function isRisky(): bool
{
return true;
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$tokensAnalyzer = new TokensAnalyzer($tokens);
foreach ($tokens->getNamespaceDeclarations() as $namespace) {
for ($index = $namespace->getScopeStartIndex(); $index < $namespace->getScopeEndIndex(); ++$index) {
if (!$tokens[$index]->isGivenKind([\T_CLASS, \T_INTERFACE]) || $tokensAnalyzer->isAnonymousClass($index)) {
continue;
}
$nameIndex = $tokens->getNextTokenOfKind($index, [[\T_STRING]]);
$startIndex = $tokens->getNextTokenOfKind($nameIndex, ['{']);
$endIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $startIndex);
$name = $tokens[$nameIndex]->getContent();
$this->replaceNameOccurrences($tokens, $namespace->getFullName(), $name, $startIndex, $endIndex);
$index = $endIndex;
}
}
}
/**
* Replace occurrences of the name of the classy element by "self" (if possible).
*/
private function replaceNameOccurrences(Tokens $tokens, string $namespace, string $name, int $startIndex, int $endIndex): void
{
$tokensAnalyzer = new TokensAnalyzer($tokens);
$insideMethodSignatureUntil = null;
for ($i = $startIndex; $i < $endIndex; ++$i) {
if ($i === $insideMethodSignatureUntil) {
$insideMethodSignatureUntil = null;
}
$token = $tokens[$i];
// skip anonymous classes
if ($token->isGivenKind(\T_CLASS) && $tokensAnalyzer->isAnonymousClass($i)) {
$i = $tokens->getNextTokenOfKind($i, ['{']);
$i = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $i);
continue;
}
if ($token->isGivenKind(\T_FN)) {
$i = $tokensAnalyzer->getLastTokenIndexOfArrowFunction($i);
$i = $tokens->getNextMeaningfulToken($i);
continue;
}
if ($token->isGivenKind(\T_FUNCTION)) {
if ($tokensAnalyzer->isLambda($i)) {
$i = $tokens->getNextTokenOfKind($i, ['{']);
$i = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $i);
continue;
}
$i = $tokens->getNextTokenOfKind($i, ['(']);
$insideMethodSignatureUntil = $tokens->getNextTokenOfKind($i, ['{', ';']);
continue;
}
if (!$token->equals([\T_STRING, $name], false)) {
continue;
}
$nextToken = $tokens[$tokens->getNextMeaningfulToken($i)];
if ($nextToken->isGivenKind(\T_NS_SEPARATOR)) {
continue;
}
$classStartIndex = $i;
$prevToken = $tokens[$tokens->getPrevMeaningfulToken($i)];
if ($prevToken->isGivenKind(\T_NS_SEPARATOR)) {
$classStartIndex = $this->getClassStart($tokens, $i, $namespace);
if (null === $classStartIndex) {
continue;
}
$prevToken = $tokens[$tokens->getPrevMeaningfulToken($classStartIndex)];
}
if ($prevToken->isGivenKind(\T_STRING) || $prevToken->isObjectOperator()) {
continue;
}
if (
$prevToken->isGivenKind([\T_INSTANCEOF, \T_NEW])
|| $nextToken->isGivenKind(\T_PAAMAYIM_NEKUDOTAYIM)
|| (
null !== $insideMethodSignatureUntil
&& $i < $insideMethodSignatureUntil
&& $prevToken->equalsAny(['(', ',', [CT::T_NULLABLE_TYPE], [CT::T_TYPE_ALTERNATION], [CT::T_TYPE_COLON]])
)
) {
for ($j = $classStartIndex; $j < $i; ++$j) {
$tokens->clearTokenAndMergeSurroundingWhitespace($j);
}
$tokens[$i] = new Token([\T_STRING, 'self']);
}
}
}
private function getClassStart(Tokens $tokens, int $index, string $namespace): ?int
{
$namespace = ('' !== $namespace ? '\\'.$namespace : '').'\\';
foreach (array_reverse(Preg::split('/(\\\)/', $namespace, -1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE)) as $piece) {
$index = $tokens->getPrevMeaningfulToken($index);
if ('\\' === $piece) {
if (!$tokens[$index]->isGivenKind(\T_NS_SEPARATOR)) {
return null;
}
} elseif (!$tokens[$index]->equals([\T_STRING, $piece], false)) {
return null;
}
}
return $index;
}
}

View File

@@ -0,0 +1,215 @@
<?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\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\FixerDefinition\VersionSpecification;
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
final class SelfStaticAccessorFixer extends AbstractFixer
{
private const CLASSY_TYPES = [\T_CLASS, FCT::T_ENUM];
private const CLASSY_TOKENS_OF_INTEREST = [[\T_CLASS], [FCT::T_ENUM]];
private TokensAnalyzer $tokensAnalyzer;
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Inside an enum or `final`/anonymous class, `self` should be preferred over `static`.',
[
new CodeSample(
'<?php
final class Sample
{
private static $A = 1;
public function getBar()
{
return static::class.static::test().static::$A;
}
private static function test()
{
return \'test\';
}
}
'
),
new CodeSample(
'<?php
final class Foo
{
public function bar()
{
return new static();
}
}
'
),
new CodeSample(
'<?php
final class Foo
{
public function isBar()
{
return $foo instanceof static;
}
}
'
),
new CodeSample(
'<?php
$a = new class() {
public function getBar()
{
return static::class;
}
};
'
),
new VersionSpecificCodeSample(
'<?php
enum Foo
{
public const A = 123;
public static function bar(): void
{
echo static::A;
}
}
',
new VersionSpecification(8_01_00)
),
]
);
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(\T_STATIC)
&& $tokens->isAnyTokenKindsFound(self::CLASSY_TYPES)
&& $tokens->isAnyTokenKindsFound([\T_DOUBLE_COLON, \T_NEW, \T_INSTANCEOF]);
}
/**
* {@inheritdoc}
*
* Must run after FinalClassFixer, FinalInternalClassFixer, FunctionToConstantFixer, PhpUnitTestCaseStaticMethodCallsFixer.
*/
public function getPriority(): int
{
return -10;
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$this->tokensAnalyzer = new TokensAnalyzer($tokens);
$classyIndex = $tokens->getNextTokenOfKind(0, self::CLASSY_TOKENS_OF_INTEREST);
while (null !== $classyIndex) {
if ($tokens[$classyIndex]->isGivenKind(\T_CLASS)) {
$modifiers = $this->tokensAnalyzer->getClassyModifiers($classyIndex);
if (
isset($modifiers['final'])
|| $this->tokensAnalyzer->isAnonymousClass($classyIndex)
) {
$classyIndex = $this->fixClassy($tokens, $classyIndex);
}
} else {
$classyIndex = $this->fixClassy($tokens, $classyIndex);
}
$classyIndex = $tokens->getNextTokenOfKind($classyIndex, self::CLASSY_TOKENS_OF_INTEREST);
}
}
private function fixClassy(Tokens $tokens, int $index): int
{
$index = $tokens->getNextTokenOfKind($index, ['{']);
$classOpenCount = 1;
while ($classOpenCount > 0) {
++$index;
if ($tokens[$index]->equals('{')) {
++$classOpenCount;
continue;
}
if ($tokens[$index]->equals('}')) {
--$classOpenCount;
continue;
}
if ($tokens[$index]->isGivenKind(\T_FUNCTION)) {
// do not fix inside lambda
if ($this->tokensAnalyzer->isLambda($index)) {
// figure out where the lambda starts
$index = $tokens->getNextTokenOfKind($index, ['{']);
$openCount = 1;
do {
$index = $tokens->getNextTokenOfKind($index, ['}', '{', [\T_CLASS]]);
if ($tokens[$index]->equals('}')) {
--$openCount;
} elseif ($tokens[$index]->equals('{')) {
++$openCount;
} else {
$index = $this->fixClassy($tokens, $index);
}
} while ($openCount > 0);
}
continue;
}
if ($tokens[$index]->isGivenKind([\T_NEW, \T_INSTANCEOF])) {
$index = $tokens->getNextMeaningfulToken($index);
if ($tokens[$index]->isGivenKind(\T_STATIC)) {
$tokens[$index] = new Token([\T_STRING, 'self']);
}
continue;
}
if (!$tokens[$index]->isGivenKind(\T_STATIC)) {
continue;
}
$staticIndex = $index;
$index = $tokens->getNextMeaningfulToken($index);
if (!$tokens[$index]->isGivenKind(\T_DOUBLE_COLON)) {
continue;
}
$tokens[$staticIndex] = new Token([\T_STRING, 'self']);
}
return $index;
}
}

View File

@@ -0,0 +1,237 @@
<?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\FixerConfiguration\AllowedValueSubset;
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\Preg;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
/**
* Fixer for rules defined in PSR2 ¶4.2.
*
* @phpstan-type _AutogeneratedInputConfiguration array{
* elements?: list<'const'|'property'>,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* elements: list<'const'|'property'>,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Javier Spagnoletti <phansys@gmail.com>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class SingleClassElementPerStatementFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound(Token::getClassyTokenKinds());
}
/**
* {@inheritdoc}
*
* Must run before ClassAttributesSeparationFixer.
*/
public function getPriority(): int
{
return 56;
}
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'There MUST NOT be more than one property or constant declared per statement.',
[
new CodeSample(
'<?php
final class Example
{
const FOO_1 = 1, FOO_2 = 2;
private static $bar1 = array(1,2,3), $bar2 = [1,2,3];
}
'
),
new CodeSample(
'<?php
final class Example
{
const FOO_1 = 1, FOO_2 = 2;
private static $bar1 = array(1,2,3), $bar2 = [1,2,3];
}
',
['elements' => ['property']]
),
]
);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$analyzer = new TokensAnalyzer($tokens);
$elements = array_reverse($analyzer->getClassyElements(), true);
foreach ($elements as $index => $element) {
if (!\in_array($element['type'], $this->configuration['elements'], true)) {
continue; // not in configuration
}
$this->fixElement($tokens, $element['type'], $index);
}
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
$values = ['const', 'property'];
return new FixerConfigurationResolver([
(new FixerOptionBuilder('elements', 'List of strings which element should be modified.'))
->setDefault($values)
->setAllowedTypes(['string[]'])
->setAllowedValues([new AllowedValueSubset($values)])
->getOption(),
]);
}
private function fixElement(Tokens $tokens, string $type, int $index): void
{
$tokensAnalyzer = new TokensAnalyzer($tokens);
$repeatIndex = $index;
while (true) {
$repeatIndex = $tokens->getNextMeaningfulToken($repeatIndex);
$repeatToken = $tokens[$repeatIndex];
if ($tokensAnalyzer->isArray($repeatIndex)) {
if ($repeatToken->isGivenKind(\T_ARRAY)) {
$repeatIndex = $tokens->getNextTokenOfKind($repeatIndex, ['(']);
$repeatIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $repeatIndex);
} else {
$repeatIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $repeatIndex);
}
continue;
}
if ($repeatToken->equalsAny([';', [CT::T_PROPERTY_HOOK_BRACE_OPEN]])) {
return; // no repeating found, no fixing needed
}
if ($repeatToken->equals(',')) {
break;
}
}
$start = $tokens->getPrevTokenOfKind($index, [';', '{', '}']);
$this->expandElement(
$tokens,
$type,
$tokens->getNextMeaningfulToken($start),
$tokens->getNextTokenOfKind($index, [';'])
);
}
private function expandElement(Tokens $tokens, string $type, int $startIndex, int $endIndex): void
{
$divisionContent = null;
if ($tokens[$startIndex - 1]->isWhitespace()) {
$divisionContent = $tokens[$startIndex - 1]->getContent();
if (Preg::match('#(\n|\r\n)#', $divisionContent, $matches)) {
$divisionContent = $matches[0].trim($divisionContent, "\r\n");
}
}
// iterate variables to split up
for ($i = $endIndex - 1; $i > $startIndex; --$i) {
$token = $tokens[$i];
if ($token->equals(')')) {
$i = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $i);
continue;
}
if ($token->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_CLOSE)) {
$i = $tokens->findBlockStart(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $i);
continue;
}
if (!$tokens[$i]->equals(',')) {
continue;
}
$tokens[$i] = new Token(';');
if ($tokens[$i + 1]->isWhitespace()) {
$tokens->clearAt($i + 1);
}
if (null !== $divisionContent && '' !== $divisionContent) {
$tokens->insertAt($i + 1, new Token([\T_WHITESPACE, $divisionContent]));
}
// collect modifiers
$sequence = $this->getModifiersSequences($tokens, $type, $startIndex, $endIndex);
$tokens->insertAt($i + 2, $sequence);
}
}
/**
* @return list<Token>
*/
private function getModifiersSequences(Tokens $tokens, string $type, int $startIndex, int $endIndex): array
{
if ('property' === $type) {
$tokenKinds = [\T_PUBLIC, \T_PROTECTED, \T_PRIVATE, \T_STATIC, \T_VAR, \T_STRING, \T_NS_SEPARATOR, CT::T_NULLABLE_TYPE, CT::T_ARRAY_TYPEHINT, CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION, FCT::T_READONLY, FCT::T_PRIVATE_SET, FCT::T_PROTECTED_SET, FCT::T_PUBLIC_SET];
} else {
$tokenKinds = [\T_PUBLIC, \T_PROTECTED, \T_PRIVATE, \T_CONST];
}
$sequence = [];
for ($i = $startIndex; $i < $endIndex - 1; ++$i) {
if ($tokens[$i]->isComment()) {
continue;
}
if (!$tokens[$i]->isWhitespace() && !$tokens[$i]->isGivenKind($tokenKinds)) {
break;
}
$sequence[] = clone $tokens[$i];
}
return $sequence;
}
}

View File

@@ -0,0 +1,116 @@
<?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\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
final class SingleTraitInsertPerStatementFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Each trait `use` must be done as single statement.',
[
new CodeSample(
'<?php
final class Example
{
use Foo, Bar;
}
'
),
]
);
}
/**
* {@inheritdoc}
*
* Must run before BracesFixer, SpaceAfterSemicolonFixer.
*/
public function getPriority(): int
{
return 36;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(CT::T_USE_TRAIT);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
for ($index = \count($tokens) - 1; 1 < $index; --$index) {
if ($tokens[$index]->isGivenKind(CT::T_USE_TRAIT)) {
$candidates = $this->getCandidates($tokens, $index);
if (\count($candidates) > 0) {
$this->fixTraitUse($tokens, $index, $candidates);
}
}
}
}
/**
* @param list<int> $candidates ',' indices to fix
*/
private function fixTraitUse(Tokens $tokens, int $useTraitIndex, array $candidates): void
{
foreach ($candidates as $commaIndex) {
$inserts = [
new Token([CT::T_USE_TRAIT, 'use']),
new Token([\T_WHITESPACE, ' ']),
];
$nextImportStartIndex = $tokens->getNextMeaningfulToken($commaIndex);
if ($tokens[$nextImportStartIndex - 1]->isWhitespace()) {
if (Preg::match('/\R/', $tokens[$nextImportStartIndex - 1]->getContent())) {
array_unshift($inserts, clone $tokens[$useTraitIndex - 1]);
}
$tokens->clearAt($nextImportStartIndex - 1);
}
$tokens[$commaIndex] = new Token(';');
$tokens->insertAt($nextImportStartIndex, $inserts);
}
}
/**
* @return list<int>
*/
private function getCandidates(Tokens $tokens, int $index): array
{
$indices = [];
$index = $tokens->getNextTokenOfKind($index, [',', ';', '{']);
while (!$tokens[$index]->equals(';')) {
if ($tokens[$index]->equals('{')) {
return []; // do not fix use cases with grouping
}
$indices[] = $index;
$index = $tokens->getNextTokenOfKind($index, [',', ';', '{']);
}
return array_reverse($indices);
}
}

View File

@@ -0,0 +1,281 @@
<?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\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
/**
* @author Filippo Tessarotto <zoeslam@gmail.com>
*/
final class StaticPrivateMethodFixer extends AbstractFixer
{
/**
* @var array<string, true>
*/
private const MAGIC_METHODS = [
'__clone' => true,
'__construct' => true,
'__destruct' => true,
'__wakeup' => true,
];
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Converts private methods to `static` where possible.',
[
new CodeSample(
'<?php
class Foo
{
public function bar()
{
return $this->baz();
}
private function baz()
{
return 1;
}
}
'
),
],
null,
'Risky when the method:'
.' contains dynamic generated calls to the instance,'
.' is dynamically referenced,'
.' is referenced inside a Trait the class uses.'
);
}
/**
* {@inheritdoc}
*
* Must run before StaticLambdaFixer.
* Must run after ProtectedToPrivateFixer.
*/
public function getPriority(): int
{
return 1;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAllTokenKindsFound([\T_CLASS, \T_PRIVATE, \T_FUNCTION]);
}
public function isRisky(): bool
{
return true;
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$tokensAnalyzer = new TokensAnalyzer($tokens);
do {
$anythingChanged = false;
$end = \count($tokens) - 3; // min. number of tokens to form a class candidate to fix
for ($index = $end; $index > 0; --$index) {
if (!$tokens[$index]->isGivenKind(\T_CLASS)) {
continue;
}
$classOpen = $tokens->getNextTokenOfKind($index, ['{']);
$classClose = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $classOpen);
$anythingChanged |= $this->fixClass($tokens, $tokensAnalyzer, $classOpen, $classClose);
}
} while ($anythingChanged);
}
private function fixClass(Tokens $tokens, TokensAnalyzer $tokensAnalyzer, int $classOpen, int $classClose): bool
{
$fixedMethods = [];
foreach ($this->getClassMethods($tokens, $classOpen, $classClose) as $methodData) {
[$functionKeywordIndex, $methodOpen, $methodClose] = $methodData;
if ($this->skipMethod($tokens, $tokensAnalyzer, $functionKeywordIndex, $methodOpen, $methodClose)) {
continue;
}
$methodNameIndex = $tokens->getNextMeaningfulToken($functionKeywordIndex);
$methodName = $tokens[$methodNameIndex]->getContent();
$fixedMethods[$methodName] = true;
$tokens->insertSlices([$functionKeywordIndex => [new Token([\T_STATIC, 'static']), new Token([\T_WHITESPACE, ' '])]]);
}
if (0 === \count($fixedMethods)) {
return false;
}
$classClose = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $classOpen);
foreach ($this->getClassMethods($tokens, $classOpen, $classClose) as $methodData) {
[, $methodOpen, $methodClose] = $methodData;
$this->fixReferencesInFunction($tokens, $tokensAnalyzer, $methodOpen, $methodClose, $fixedMethods);
}
return true;
}
private function skipMethod(Tokens $tokens, TokensAnalyzer $tokensAnalyzer, int $functionKeywordIndex, int $methodOpen, int $methodClose): bool
{
$methodNameIndex = $tokens->getNextMeaningfulToken($functionKeywordIndex);
$methodName = strtolower($tokens[$methodNameIndex]->getContent());
if (isset(self::MAGIC_METHODS[$methodName])) {
return true;
}
$prevTokenIndex = $tokens->getPrevMeaningfulToken($functionKeywordIndex);
if ($tokens[$prevTokenIndex]->isGivenKind(\T_FINAL)) {
$prevTokenIndex = $tokens->getPrevMeaningfulToken($prevTokenIndex);
}
if (!$tokens[$prevTokenIndex]->isGivenKind(\T_PRIVATE)) {
return true;
}
$prePrevTokenIndex = $tokens->getPrevMeaningfulToken($prevTokenIndex);
if ($tokens[$prePrevTokenIndex]->isGivenKind(\T_STATIC)) {
return true;
}
for ($index = $methodOpen + 1; $index < $methodClose - 1; ++$index) {
if ($tokens[$index]->isGivenKind(\T_CLASS) && $tokensAnalyzer->isAnonymousClass($index)) {
$anonymousClassOpen = $tokens->getNextTokenOfKind($index, ['{']);
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $anonymousClassOpen);
continue;
}
if ($tokens[$index]->isGivenKind(\T_FUNCTION)) {
return true;
}
if ($tokens[$index]->equals([\T_VARIABLE, '$this'])) {
$operatorIndex = $tokens->getNextMeaningfulToken($index);
$methodNameIndex = $tokens->getNextMeaningfulToken($operatorIndex);
$argumentsBraceIndex = $tokens->getNextMeaningfulToken($methodNameIndex);
if (
!$tokens[$operatorIndex]->isGivenKind(\T_OBJECT_OPERATOR)
|| $methodName !== $tokens[$methodNameIndex]->getContent()
|| !$tokens[$argumentsBraceIndex]->equals('(')
) {
return true;
}
}
if ($tokens[$index]->equals([\T_STRING, 'debug_backtrace'])) {
return true;
}
}
return false;
}
/**
* @param array<string, bool> $fixedMethods
*/
private function fixReferencesInFunction(Tokens $tokens, TokensAnalyzer $tokensAnalyzer, int $methodOpen, int $methodClose, array $fixedMethods): void
{
for ($index = $methodOpen + 1; $index < $methodClose - 1; ++$index) {
if ($tokens[$index]->isGivenKind(\T_FUNCTION)) {
$prevIndex = $tokens->getPrevMeaningfulToken($index);
$closureStart = $tokens->getNextTokenOfKind($index, ['{']);
$closureEnd = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $closureStart);
if (!$tokens[$prevIndex]->isGivenKind(\T_STATIC)) {
$this->fixReferencesInFunction($tokens, $tokensAnalyzer, $closureStart, $closureEnd, $fixedMethods);
}
$index = $closureEnd;
continue;
}
if ($tokens[$index]->isGivenKind(\T_CLASS) && $tokensAnalyzer->isAnonymousClass($index)) {
$anonymousClassOpen = $tokens->getNextTokenOfKind($index, ['{']);
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $anonymousClassOpen);
continue;
}
if (!$tokens[$index]->equals([\T_VARIABLE, '$this'])) {
continue;
}
$objectOperatorIndex = $tokens->getNextMeaningfulToken($index);
if (!$tokens[$objectOperatorIndex]->isGivenKind(\T_OBJECT_OPERATOR)) {
continue;
}
$methodNameIndex = $tokens->getNextMeaningfulToken($objectOperatorIndex);
$argumentsBraceIndex = $tokens->getNextMeaningfulToken($methodNameIndex);
if (!$tokens[$argumentsBraceIndex]->equals('(')) {
continue;
}
$currentMethodName = $tokens[$methodNameIndex]->getContent();
if (!isset($fixedMethods[$currentMethodName])) {
continue;
}
$tokens[$index] = new Token([\T_STRING, 'self']);
$tokens[$objectOperatorIndex] = new Token([\T_DOUBLE_COLON, '::']);
}
}
/**
* @return list<array{int, int, int}>
*/
private function getClassMethods(Tokens $tokens, int $classOpen, int $classClose): array
{
$methods = [];
for ($index = $classClose - 1; $index > $classOpen + 1; --$index) {
if ($tokens[$index]->equals('}')) {
$index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
continue;
}
if (!$tokens[$index]->isGivenKind(\T_FUNCTION)) {
continue;
}
$functionKeywordIndex = $index;
$prevTokenIndex = $tokens->getPrevMeaningfulToken($functionKeywordIndex);
$prevPrevTokenIndex = $tokens->getPrevMeaningfulToken($prevTokenIndex);
if ($tokens[$prevTokenIndex]->isGivenKind(\T_ABSTRACT) || $tokens[$prevPrevTokenIndex]->isGivenKind(\T_ABSTRACT)) {
continue;
}
$methodOpen = $tokens->getNextTokenOfKind($functionKeywordIndex, ['{']);
$methodClose = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $methodOpen);
$methods[] = [$functionKeywordIndex, $methodOpen, $methodClose];
}
return $methods;
}
}

View File

@@ -0,0 +1,305 @@
<?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\FixerConfiguration\AllowedValueSubset;
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\FixerDefinition\VersionSpecification;
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
/**
* Fixer for rules defined in PSR2 ¶4.3, ¶4.5.
*
* @phpstan-type _AutogeneratedInputConfiguration array{
* elements?: list<'const'|'method'|'property'>,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* elements: list<'const'|'method'|'property'>,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class VisibilityRequiredFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
private const PROPERTY_TYPE_DECLARATION_KINDS = [\T_STRING, \T_NS_SEPARATOR, CT::T_NULLABLE_TYPE, CT::T_ARRAY_TYPEHINT, CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION, CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_OPEN, CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_CLOSE];
private const EXPECTED_KINDS_GENERIC = [\T_ABSTRACT, \T_FINAL, \T_PRIVATE, \T_PROTECTED, \T_PUBLIC, \T_STATIC, \T_VAR, CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC, CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED, CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE, FCT::T_READONLY, FCT::T_PRIVATE_SET, FCT::T_PROTECTED_SET, FCT::T_PUBLIC_SET];
private const EXPECTED_KINDS_PROPERTY_KINDS = [...self::EXPECTED_KINDS_GENERIC, ...self::PROPERTY_TYPE_DECLARATION_KINDS];
/**
* @var list<'const'|'method'|'promoted_property'|'property'>
*/
private array $elements = ['const', 'method', 'property'];
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Classes, constants, properties, and methods MUST have visibility declared, and keyword modifiers MUST be in the following order:'
.' inheritance modifier (`abstract` or `final`),'
.' visibility modifier (`public`, `protected`, or `private`),'
.' set-visibility modifier (`public(set)`, `protected(set)`, or `private(set)`),'
.' scope modifier (`static`),'
.' mutation modifier (`readonly`),'
.' type declaration, name.',
[
new CodeSample(
'<?php
abstract class ClassName
{
const SAMPLE = 1;
var $a;
protected string $foo;
static protected int $beep;
static public final function bar() {}
protected abstract function zim();
function zex() {}
}
',
),
new VersionSpecificCodeSample(
'<?php
abstract class ClassName
{
const SAMPLE = 1;
var $a;
readonly protected string $foo;
static protected int $beep;
static public final function bar() {}
protected abstract function zim();
function zex() {}
}
readonly final class ValueObject
{
// ...
}
',
new VersionSpecification(8_02_00)
),
new VersionSpecificCodeSample(
'<?php
abstract class ClassName
{
const SAMPLE = 1;
var $a;
protected abstract string $bar { get => "a"; set; }
readonly final protected string $foo;
static protected final int $beep;
static public final function bar() {}
protected abstract function zim();
function zex() {}
}
readonly final class ValueObject
{
// ...
}
',
new VersionSpecification(8_04_00)
),
new CodeSample(
'<?php
class Sample
{
const SAMPLE = 1;
}
',
['elements' => ['const']]
),
]
);
}
/**
* {@inheritdoc}
*
* Must run before ClassAttributesSeparationFixer.
*/
public function getPriority(): int
{
return 56;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound(Token::getClassyTokenKinds());
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
$elements = ['const', 'method', 'property'];
return new FixerConfigurationResolver([
(new FixerOptionBuilder('elements', 'The structural elements to fix.'))
->setAllowedTypes(['string[]'])
->setAllowedValues([new AllowedValueSubset($elements)])
->setDefault($elements)
->getOption(),
]);
}
protected function configurePostNormalisation(): void
{
$this->elements = $this->configuration['elements'];
if (\in_array('property', $this->elements, true)) {
$this->elements[] = 'promoted_property';
}
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$tokensAnalyzer = new TokensAnalyzer($tokens);
foreach (array_reverse($tokensAnalyzer->getClassyElements(), true) as $index => $element) {
if (!\in_array($element['type'], $this->elements, true)) {
continue;
}
$abstractFinalIndex = null;
$visibilityIndex = null;
$visibilitySetIndex = null;
$staticIndex = null;
$typeIndex = null;
$readOnlyIndex = null;
$prevIndex = $tokens->getPrevMeaningfulToken($index);
$expectedKinds = 'property' === $element['type'] || 'promoted_property' === $element['type']
? self::EXPECTED_KINDS_PROPERTY_KINDS
: self::EXPECTED_KINDS_GENERIC;
while ($tokens[$prevIndex]->isGivenKind($expectedKinds) || $tokens[$prevIndex]->equals('&')) {
if ($tokens[$prevIndex]->isGivenKind([\T_ABSTRACT, \T_FINAL])) {
$abstractFinalIndex = $prevIndex;
} elseif ($tokens[$prevIndex]->isGivenKind(\T_STATIC)) {
$staticIndex = $prevIndex;
} elseif ($tokens[$prevIndex]->isGivenKind(FCT::T_READONLY)) {
$readOnlyIndex = $prevIndex;
} elseif ($tokens[$prevIndex]->isGivenKind([FCT::T_PRIVATE_SET, FCT::T_PROTECTED_SET, FCT::T_PUBLIC_SET])) {
$visibilitySetIndex = $prevIndex;
} elseif ($tokens[$prevIndex]->isGivenKind(self::PROPERTY_TYPE_DECLARATION_KINDS)) {
$typeIndex = $prevIndex;
} elseif (!$tokens[$prevIndex]->equals('&')) {
$visibilityIndex = $prevIndex;
}
$prevIndex = $tokens->getPrevMeaningfulToken($prevIndex);
}
if (null !== $typeIndex) {
$index = $typeIndex;
}
if ('property' === $element['type'] && $tokens[$prevIndex]->equals(',')) {
continue;
}
$swapIndex = $staticIndex ?? $readOnlyIndex; // "static" property cannot be "readonly", so there can always be at most one swap
if (null !== $swapIndex) {
if ($this->isKeywordPlacedProperly($tokens, $swapIndex, $index)) {
$index = $swapIndex;
} else {
$this->moveTokenAndEnsureSingleSpaceFollows($tokens, $swapIndex, $index);
}
}
if (null !== $visibilitySetIndex) {
if ($this->isKeywordPlacedProperly($tokens, $visibilitySetIndex, $index)) {
$index = $visibilitySetIndex;
} else {
$this->moveTokenAndEnsureSingleSpaceFollows($tokens, $visibilitySetIndex, $index);
}
}
if (null === $visibilityIndex) {
$tokens->insertAt($index, [new Token(['promoted_property' === $element['type'] ? CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC : \T_PUBLIC, 'public']), new Token([\T_WHITESPACE, ' '])]);
} else {
if ($tokens[$visibilityIndex]->isGivenKind(\T_VAR)) {
$tokens[$visibilityIndex] = new Token([\T_PUBLIC, 'public']);
}
if ($this->isKeywordPlacedProperly($tokens, $visibilityIndex, $index)) {
$index = $visibilityIndex;
} else {
$this->moveTokenAndEnsureSingleSpaceFollows($tokens, $visibilityIndex, $index);
}
}
if (null === $abstractFinalIndex) {
continue;
}
if ($this->isKeywordPlacedProperly($tokens, $abstractFinalIndex, $index)) {
continue;
}
$this->moveTokenAndEnsureSingleSpaceFollows($tokens, $abstractFinalIndex, $index);
}
}
private function isKeywordPlacedProperly(Tokens $tokens, int $keywordIndex, int $comparedIndex): bool
{
return ' ' === $tokens[$keywordIndex + 1]->getContent()
&& (
$keywordIndex + 2 === $comparedIndex
|| $keywordIndex + 3 === $comparedIndex && $tokens[$keywordIndex + 2]->equals('&')
);
}
private function moveTokenAndEnsureSingleSpaceFollows(Tokens $tokens, int $fromIndex, int $toIndex): void
{
$tokens->insertAt($toIndex, [$tokens[$fromIndex], new Token([\T_WHITESPACE, ' '])]);
$tokens->clearAt($fromIndex);
if ($tokens[$fromIndex + 1]->isWhitespace()) {
$tokens->clearAt($fromIndex + 1);
}
}
}