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,247 @@
<?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\ControlStructure;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Analyzer\AlternativeSyntaxAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
final class ControlStructureBracesFixer extends AbstractFixer
{
private const CONTROL_TOKENS = [
\T_DECLARE,
\T_DO,
\T_ELSE,
\T_ELSEIF,
\T_FINALLY,
\T_FOR,
\T_FOREACH,
\T_IF,
\T_WHILE,
\T_TRY,
\T_CATCH,
\T_SWITCH,
];
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'The body of each control structure MUST be enclosed within braces.',
[new CodeSample("<?php\nif (foo()) echo 'Hello!';\n")]
);
}
public function isCandidate(Tokens $tokens): bool
{
return true;
}
/**
* {@inheritdoc}
*
* Must run before BracesPositionFixer, ControlStructureContinuationPositionFixer, CurlyBracesPositionFixer, NoMultipleStatementsPerLineFixer.
*/
public function getPriority(): int
{
return 1;
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$alternativeSyntaxAnalyzer = new AlternativeSyntaxAnalyzer();
for ($index = $tokens->count() - 1; 0 <= $index; --$index) {
$token = $tokens[$index];
if (!$token->isGivenKind(self::CONTROL_TOKENS)) {
continue;
}
if (
$token->isGivenKind(\T_ELSE)
&& $tokens[$tokens->getNextMeaningfulToken($index)]->isGivenKind(\T_IF)
) {
continue;
}
$parenthesisEndIndex = $this->findParenthesisEnd($tokens, $index);
$nextAfterParenthesisEndIndex = $tokens->getNextMeaningfulToken($parenthesisEndIndex);
$tokenAfterParenthesis = $tokens[$nextAfterParenthesisEndIndex];
if ($tokenAfterParenthesis->equalsAny([';', '{', ':', [\T_CLOSE_TAG]])) {
continue;
}
$statementEndIndex = null;
if ($tokenAfterParenthesis->isGivenKind([\T_IF, \T_FOR, \T_FOREACH, \T_SWITCH, \T_WHILE])) {
$tokenAfterParenthesisBlockEnd = $tokens->findBlockEnd( // go to ')'
Tokens::BLOCK_TYPE_PARENTHESIS_BRACE,
$tokens->getNextMeaningfulToken($nextAfterParenthesisEndIndex)
);
if ($tokens[$tokens->getNextMeaningfulToken($tokenAfterParenthesisBlockEnd)]->equals(':')) {
$statementEndIndex = $alternativeSyntaxAnalyzer->findAlternativeSyntaxBlockEnd($tokens, $nextAfterParenthesisEndIndex);
$tokenAfterStatementEndIndex = $tokens->getNextMeaningfulToken($statementEndIndex);
if ($tokens[$tokenAfterStatementEndIndex]->equals(';')) {
$statementEndIndex = $tokenAfterStatementEndIndex;
}
}
}
if (null === $statementEndIndex) {
$statementEndIndex = $this->findStatementEnd($tokens, $parenthesisEndIndex);
}
$tokensToInsertAfterStatement = [
new Token([\T_WHITESPACE, ' ']),
new Token('}'),
];
if (!$tokens[$statementEndIndex]->equalsAny([';', '}'])) {
array_unshift($tokensToInsertAfterStatement, new Token(';'));
}
$tokens->insertSlices([$statementEndIndex + 1 => $tokensToInsertAfterStatement]);
// insert opening brace
$tokens->insertSlices([$parenthesisEndIndex + 1 => [
new Token([\T_WHITESPACE, ' ']),
new Token('{'),
]]);
}
}
private function findParenthesisEnd(Tokens $tokens, int $structureTokenIndex): int
{
$nextIndex = $tokens->getNextMeaningfulToken($structureTokenIndex);
$nextToken = $tokens[$nextIndex];
if (!$nextToken->equals('(')) {
return $structureTokenIndex;
}
return $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $nextIndex);
}
private function findStatementEnd(Tokens $tokens, int $parenthesisEndIndex): int
{
$nextIndex = $tokens->getNextMeaningfulToken($parenthesisEndIndex);
if (null === $nextIndex) {
return $parenthesisEndIndex;
}
$nextToken = $tokens[$nextIndex];
if ($nextToken->equals('{')) {
return $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $nextIndex);
}
if ($nextToken->isGivenKind(self::CONTROL_TOKENS)) {
$parenthesisEndIndex = $this->findParenthesisEnd($tokens, $nextIndex);
$endIndex = $this->findStatementEnd($tokens, $parenthesisEndIndex);
if ($nextToken->isGivenKind([\T_IF, \T_TRY, \T_DO])) {
$openingTokenKind = $nextToken->getId();
while (true) {
$nextIndex = $tokens->getNextMeaningfulToken($endIndex);
if (null !== $nextIndex && $tokens[$nextIndex]->isGivenKind($this->getControlContinuationTokensForOpeningToken($openingTokenKind))) {
$parenthesisEndIndex = $this->findParenthesisEnd($tokens, $nextIndex);
$endIndex = $this->findStatementEnd($tokens, $parenthesisEndIndex);
if ($tokens[$nextIndex]->isGivenKind($this->getFinalControlContinuationTokensForOpeningToken($openingTokenKind))) {
return $endIndex;
}
} else {
break;
}
}
}
return $endIndex;
}
$index = $parenthesisEndIndex;
while (true) {
$token = $tokens[++$index];
// if there is some block in statement (eg lambda function) we need to skip it
if ($token->equals('{')) {
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
continue;
}
if ($token->equals(';')) {
return $index;
}
if ($token->isGivenKind(\T_CLOSE_TAG)) {
return $tokens->getPrevNonWhitespace($index);
}
}
}
/**
* @return list<int>
*/
private function getControlContinuationTokensForOpeningToken(int $openingTokenKind): array
{
if (\T_IF === $openingTokenKind) {
return [
\T_ELSE,
\T_ELSEIF,
];
}
if (\T_DO === $openingTokenKind) {
return [\T_WHILE];
}
if (\T_TRY === $openingTokenKind) {
return [
\T_CATCH,
\T_FINALLY,
];
}
return [];
}
/**
* @return list<int>
*/
private function getFinalControlContinuationTokensForOpeningToken(int $openingTokenKind): array
{
if (\T_IF === $openingTokenKind) {
return [\T_ELSE];
}
if (\T_TRY === $openingTokenKind) {
return [\T_FINALLY];
}
return [];
}
}

View File

@@ -0,0 +1,157 @@
<?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\ControlStructure;
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\Analyzer\WhitespacesAnalyzer;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @phpstan-type _AutogeneratedInputConfiguration array{
* position?: 'next_line'|'same_line',
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* position: 'next_line'|'same_line',
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*/
final class ControlStructureContinuationPositionFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
/**
* @internal
*/
public const NEXT_LINE = 'next_line';
/**
* @internal
*/
public const SAME_LINE = 'same_line';
private const CONTROL_CONTINUATION_TOKENS = [
\T_CATCH,
\T_ELSE,
\T_ELSEIF,
\T_FINALLY,
\T_WHILE,
];
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Control structure continuation keyword must be on the configured line.',
[
new CodeSample(
'<?php
if ($baz == true) {
echo "foo";
}
else {
echo "bar";
}
'
),
new CodeSample(
'<?php
if ($baz == true) {
echo "foo";
} else {
echo "bar";
}
',
['position' => self::NEXT_LINE]
),
]
);
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound(self::CONTROL_CONTINUATION_TOKENS);
}
/**
* {@inheritdoc}
*
* Must run after ControlStructureBracesFixer.
*/
public function getPriority(): int
{
return parent::getPriority();
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('position', 'The position of the keyword that continues the control structure.'))
->setAllowedValues([self::NEXT_LINE, self::SAME_LINE])
->setDefault(self::SAME_LINE)
->getOption(),
]);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$this->fixControlContinuationBraces($tokens);
}
private function fixControlContinuationBraces(Tokens $tokens): void
{
for ($index = \count($tokens) - 1; 0 < $index; --$index) {
$token = $tokens[$index];
if (!$token->isGivenKind(self::CONTROL_CONTINUATION_TOKENS)) {
continue;
}
$prevIndex = $tokens->getPrevNonWhitespace($index);
$prevToken = $tokens[$prevIndex];
if (!$prevToken->equals('}')) {
continue;
}
if ($token->isGivenKind(\T_WHILE)) {
$prevIndex = $tokens->getPrevMeaningfulToken(
$tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $prevIndex)
);
if (!$tokens[$prevIndex]->isGivenKind(\T_DO)) {
continue;
}
}
$tokens->ensureWhitespaceAtIndex(
$index - 1,
1,
self::NEXT_LINE === $this->configuration['position']
? $this->whitespacesConfig->getLineEnding().WhitespacesAnalyzer::detectIndent($tokens, $index)
: ' '
);
}
}
}

View File

@@ -0,0 +1,98 @@
<?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\ControlStructure;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* Fixer for rules defined in PSR2 ¶5.1.
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class ElseifFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'The keyword `elseif` should be used instead of `else if` so that all control keywords look like single words.',
[new CodeSample("<?php\nif (\$a) {\n} else if (\$b) {\n}\n")]
);
}
/**
* {@inheritdoc}
*
* Must run after NoAlternativeSyntaxFixer.
*/
public function getPriority(): int
{
return 40;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAllTokenKindsFound([\T_IF, \T_ELSE]);
}
/**
* Replace all `else if` (T_ELSE T_IF) with `elseif` (T_ELSEIF).
*
* {@inheritdoc}
*/
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
foreach ($tokens as $index => $token) {
if (!$token->isGivenKind(\T_ELSE)) {
continue;
}
$ifTokenIndex = $tokens->getNextMeaningfulToken($index);
// if next meaningful token is not T_IF - continue searching, this is not the case for fixing
if (!$tokens[$ifTokenIndex]->isGivenKind(\T_IF)) {
continue;
}
// if next meaningful token is T_IF, but uses an alternative syntax - this is not the case for fixing neither
$conditionEndBraceIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $tokens->getNextMeaningfulToken($ifTokenIndex));
$afterConditionIndex = $tokens->getNextMeaningfulToken($conditionEndBraceIndex);
if ($tokens[$afterConditionIndex]->equals(':')) {
continue;
}
// now we have T_ELSE following by T_IF with no alternative syntax so we could fix this
// 1. clear whitespaces between T_ELSE and T_IF
$tokens->clearAt($index + 1);
// 2. change token from T_ELSE into T_ELSEIF
$tokens[$index] = new Token([\T_ELSEIF, 'elseif']);
// 3. clear succeeding T_IF
$tokens->clearAt($ifTokenIndex);
$beforeIfTokenIndex = $tokens->getPrevNonWhitespace($ifTokenIndex);
// 4. clear extra whitespace after T_IF in T_COMMENT,T_WHITESPACE?,T_IF,T_WHITESPACE sequence
if ($tokens[$beforeIfTokenIndex]->isComment() && $tokens[$ifTokenIndex + 1]->isWhitespace()) {
$tokens->clearAt($ifTokenIndex + 1);
}
}
}
}

View File

@@ -0,0 +1,139 @@
<?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\ControlStructure;
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;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
/**
* @phpstan-type _AutogeneratedInputConfiguration array{
* style?: 'braces'|'semicolon',
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* style: 'braces'|'semicolon',
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*/
final class EmptyLoopBodyFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
private const STYLE_BRACES = 'braces';
private const STYLE_SEMICOLON = 'semicolon';
private const TOKEN_LOOP_KINDS = [\T_FOR, \T_FOREACH, \T_WHILE];
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Empty loop-body must be in configured style.',
[
new CodeSample("<?php while(foo()){}\n"),
new CodeSample(
"<?php while(foo());\n",
[
'style' => self::STYLE_BRACES,
]
),
]
);
}
/**
* {@inheritdoc}
*
* Must run before BracesFixer, NoExtraBlankLinesFixer, NoTrailingWhitespaceFixer.
* Must run after NoEmptyStatementFixer.
*/
public function getPriority(): int
{
return 39;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound(self::TOKEN_LOOP_KINDS);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
if (self::STYLE_BRACES === $this->configuration['style']) {
$analyzer = new TokensAnalyzer($tokens);
$fixLoop = static function (int $index, int $endIndex) use ($tokens, $analyzer): void {
if ($tokens[$index]->isGivenKind(\T_WHILE) && $analyzer->isWhilePartOfDoWhile($index)) {
return;
}
$semiColonIndex = $tokens->getNextMeaningfulToken($endIndex);
if (!$tokens[$semiColonIndex]->equals(';')) {
return;
}
$tokens[$semiColonIndex] = new Token('{');
$tokens->insertAt($semiColonIndex + 1, new Token('}'));
};
} else {
$fixLoop = static function (int $index, int $endIndex) use ($tokens): void {
$braceOpenIndex = $tokens->getNextMeaningfulToken($endIndex);
if (!$tokens[$braceOpenIndex]->equals('{')) {
return;
}
$braceCloseIndex = $tokens->getNextNonWhitespace($braceOpenIndex);
if (!$tokens[$braceCloseIndex]->equals('}')) {
return;
}
$tokens[$braceOpenIndex] = new Token(';');
$tokens->clearTokenAndMergeSurroundingWhitespace($braceCloseIndex);
};
}
for ($index = $tokens->count() - 1; $index > 0; --$index) {
if ($tokens[$index]->isGivenKind(self::TOKEN_LOOP_KINDS)) {
$endIndex = $tokens->getNextTokenOfKind($index, ['(']); // proceed to open '('
$endIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $endIndex); // proceed to close ')'
$fixLoop($index, $endIndex); // fix loop if needs fixing
}
}
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('style', 'Style of empty loop-bodies.'))
->setAllowedTypes(['string'])
->setAllowedValues([self::STYLE_BRACES, self::STYLE_SEMICOLON])
->setDefault(self::STYLE_SEMICOLON)
->getOption(),
]);
}
}

View File

@@ -0,0 +1,202 @@
<?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\ControlStructure;
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{
* style?: 'for'|'while',
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* style: 'for'|'while',
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*/
final class EmptyLoopConditionFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
private const STYLE_FOR = 'for';
private const STYLE_WHILE = 'while';
private const TOKEN_LOOP_KINDS = [\T_FOR, \T_WHILE];
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Empty loop-condition must be in configured style.',
[
new CodeSample("<?php\nfor(;;) {\n foo();\n}\n\ndo {\n foo();\n} while(true); // do while\n"),
new CodeSample("<?php\nwhile(true) {\n foo();\n}\n", ['style' => self::STYLE_FOR]),
]
);
}
/**
* {@inheritdoc}
*
* Must run before NoExtraBlankLinesFixer, NoTrailingWhitespaceFixer.
*/
public function getPriority(): int
{
return 1;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound(self::TOKEN_LOOP_KINDS);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
if (self::STYLE_WHILE === $this->configuration['style']) {
$candidateLoopKinds = [\T_FOR, \T_WHILE];
$replacement = [new Token([\T_WHILE, 'while']), new Token([\T_WHITESPACE, ' ']), new Token('('), new Token([\T_STRING, 'true']), new Token(')')];
$fixLoop = static function (int $index, int $openIndex, int $endIndex) use ($tokens, $replacement): void {
if (self::isForLoopWithEmptyCondition($tokens, $index, $openIndex, $endIndex)) {
self::clearNotCommentsInRange($tokens, $index, $endIndex);
self::cloneAndInsert($tokens, $index, $replacement);
} elseif (self::isWhileLoopWithEmptyCondition($tokens, $index, $openIndex, $endIndex)) {
$doIndex = self::getDoIndex($tokens, $index);
if (null !== $doIndex) {
self::clearNotCommentsInRange($tokens, $index, $tokens->getNextMeaningfulToken($endIndex)); // clear including `;`
$tokens->clearAt($doIndex);
self::cloneAndInsert($tokens, $doIndex, $replacement);
}
}
};
} else { // self::STYLE_FOR
$candidateLoopKinds = [\T_WHILE];
$replacement = [new Token([\T_FOR, 'for']), new Token('('), new Token(';'), new Token(';'), new Token(')')];
$fixLoop = static function (int $index, int $openIndex, int $endIndex) use ($tokens, $replacement): void {
if (!self::isWhileLoopWithEmptyCondition($tokens, $index, $openIndex, $endIndex)) {
return;
}
$doIndex = self::getDoIndex($tokens, $index);
if (null === $doIndex) {
self::clearNotCommentsInRange($tokens, $index, $endIndex);
self::cloneAndInsert($tokens, $index, $replacement);
} else {
self::clearNotCommentsInRange($tokens, $index, $tokens->getNextMeaningfulToken($endIndex)); // clear including `;`
$tokens->clearAt($doIndex);
self::cloneAndInsert($tokens, $doIndex, $replacement);
}
};
}
for ($index = $tokens->count() - 1; $index > 0; --$index) {
if ($tokens[$index]->isGivenKind($candidateLoopKinds)) {
$openIndex = $tokens->getNextTokenOfKind($index, ['(']); // proceed to open '('
$endIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openIndex); // proceed to close ')'
$fixLoop($index, $openIndex, $endIndex); // fix loop if needed
}
}
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('style', 'Style of empty loop-condition.'))
->setAllowedTypes(['string'])
->setAllowedValues([self::STYLE_WHILE, self::STYLE_FOR])
->setDefault(self::STYLE_WHILE)
->getOption(),
]);
}
private static function clearNotCommentsInRange(Tokens $tokens, int $indexStart, int $indexEnd): void
{
for ($i = $indexStart; $i <= $indexEnd; ++$i) {
if (!$tokens[$i]->isComment()) {
$tokens->clearTokenAndMergeSurroundingWhitespace($i);
}
}
}
/**
* @param list<Token> $replacement
*/
private static function cloneAndInsert(Tokens $tokens, int $index, array $replacement): void
{
$replacementClones = [];
foreach ($replacement as $token) {
$replacementClones[] = clone $token;
}
$tokens->insertAt($index, $replacementClones);
}
private static function getDoIndex(Tokens $tokens, int $index): ?int
{
$endIndex = $tokens->getPrevMeaningfulToken($index);
if (!$tokens[$endIndex]->equals('}')) {
return null;
}
$startIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $endIndex);
$index = $tokens->getPrevMeaningfulToken($startIndex);
return null === $index || !$tokens[$index]->isGivenKind(\T_DO) ? null : $index;
}
private static function isForLoopWithEmptyCondition(Tokens $tokens, int $index, int $openIndex, int $endIndex): bool
{
if (!$tokens[$index]->isGivenKind(\T_FOR)) {
return false;
}
$index = $tokens->getNextMeaningfulToken($openIndex);
if (null === $index || !$tokens[$index]->equals(';')) {
return false;
}
$index = $tokens->getNextMeaningfulToken($index);
return null !== $index && $tokens[$index]->equals(';') && $endIndex === $tokens->getNextMeaningfulToken($index);
}
private static function isWhileLoopWithEmptyCondition(Tokens $tokens, int $index, int $openIndex, int $endIndex): bool
{
if (!$tokens[$index]->isGivenKind(\T_WHILE)) {
return false;
}
$index = $tokens->getNextMeaningfulToken($openIndex);
return null !== $index && $tokens[$index]->equals([\T_STRING, 'true']) && $endIndex === $tokens->getNextMeaningfulToken($index);
}
}

View File

@@ -0,0 +1,151 @@
<?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\ControlStructure;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Analyzer\BlocksAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Sebastiaan Stok <s.stok@rollerscapes.net>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
* @author Kuba Werłos <werlos@gmail.com>
*/
final class IncludeFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Include/Require and file path should be divided with a single space. File path should not be placed within parentheses.',
[
new CodeSample(
'<?php
require ("sample1.php");
require_once "sample2.php";
include "sample3.php";
include_once("sample4.php");
'
),
]
);
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound([\T_REQUIRE, \T_REQUIRE_ONCE, \T_INCLUDE, \T_INCLUDE_ONCE]);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$this->clearIncludies($tokens, $this->findIncludies($tokens));
}
/**
* @param array<int, array{begin: int, braces: ?array{open: int, close: int}, end: int}> $includies
*/
private function clearIncludies(Tokens $tokens, array $includies): void
{
$blocksAnalyzer = new BlocksAnalyzer();
foreach ($includies as $includy) {
if (!$tokens[$includy['end']]->isGivenKind(\T_CLOSE_TAG)) {
$afterEndIndex = $tokens->getNextNonWhitespace($includy['end']);
if (null === $afterEndIndex || !$tokens[$afterEndIndex]->isComment()) {
$tokens->removeLeadingWhitespace($includy['end']);
}
}
$braces = $includy['braces'];
if (null !== $braces) {
$prevIndex = $tokens->getPrevMeaningfulToken($includy['begin']);
$nextIndex = $tokens->getNextMeaningfulToken($braces['close']);
// Include is also legal as function parameter or condition statement but requires being wrapped then.
if (!$tokens[$nextIndex]->equalsAny([';', [\T_CLOSE_TAG]]) && !$blocksAnalyzer->isBlock($tokens, $prevIndex, $nextIndex)) {
continue;
}
$this->removeWhitespaceAroundIfPossible($tokens, $braces['open']);
$this->removeWhitespaceAroundIfPossible($tokens, $braces['close']);
$tokens->clearTokenAndMergeSurroundingWhitespace($braces['open']);
$tokens->clearTokenAndMergeSurroundingWhitespace($braces['close']);
}
$nextIndex = $tokens->getNonEmptySibling($includy['begin'], 1);
if ($tokens[$nextIndex]->isWhitespace()) {
$tokens[$nextIndex] = new Token([\T_WHITESPACE, ' ']);
} elseif (null !== $braces || $tokens[$nextIndex]->isGivenKind([\T_VARIABLE, \T_CONSTANT_ENCAPSED_STRING, \T_COMMENT])) {
$tokens->insertAt($includy['begin'] + 1, new Token([\T_WHITESPACE, ' ']));
}
}
}
/**
* @return array<int, array{begin: int, braces: ?array{open: int, close: int}, end: int}>
*/
private function findIncludies(Tokens $tokens): array
{
$includies = [];
foreach ($tokens->findGivenKind([\T_REQUIRE, \T_REQUIRE_ONCE, \T_INCLUDE, \T_INCLUDE_ONCE]) as $includyTokens) {
foreach ($includyTokens as $index => $token) {
$includy = [
'begin' => $index,
'braces' => null,
'end' => $tokens->getNextTokenOfKind($index, [';', [\T_CLOSE_TAG]]),
];
$braceOpenIndex = $tokens->getNextMeaningfulToken($index);
if ($tokens[$braceOpenIndex]->equals('(')) {
$braceCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $braceOpenIndex);
$includy['braces'] = [
'open' => $braceOpenIndex,
'close' => $braceCloseIndex,
];
}
$includies[$index] = $includy;
}
}
krsort($includies);
return $includies;
}
private function removeWhitespaceAroundIfPossible(Tokens $tokens, int $index): void
{
$nextIndex = $tokens->getNextNonWhitespace($index);
if (null === $nextIndex || !$tokens[$nextIndex]->isComment()) {
$tokens->removeLeadingWhitespace($index);
}
$prevIndex = $tokens->getPrevNonWhitespace($index);
if (null === $prevIndex || !$tokens[$prevIndex]->isComment()) {
$tokens->removeTrailingWhitespace($index);
}
}
}

View File

@@ -0,0 +1,244 @@
<?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\ControlStructure;
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{
* fix_non_monolithic_code?: bool,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* fix_non_monolithic_code: bool,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Eddilbert Macharia <edd.cowan@gmail.com>
*/
final class NoAlternativeSyntaxFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Replace control structure alternative syntax to use braces.',
[
new CodeSample(
"<?php\nif(true):echo 't';else:echo 'f';endif;\n"
),
new CodeSample(
"<?php if (\$condition): ?>\nLorem ipsum.\n<?php endif; ?>\n",
['fix_non_monolithic_code' => true]
),
]
);
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->hasAlternativeSyntax() && (true === $this->configuration['fix_non_monolithic_code'] || $tokens->isMonolithicPhp());
}
/**
* {@inheritdoc}
*
* Must run before BracesFixer, ElseifFixer, NoSuperfluousElseifFixer, NoUnneededControlParenthesesFixer, NoUselessElseFixer, SwitchContinueToBreakFixer.
*/
public function getPriority(): int
{
return 42;
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('fix_non_monolithic_code', 'Whether to also fix code with inline HTML.'))
->setAllowedTypes(['bool'])
->setDefault(true) // @TODO change to "false" on next major 4.0
->getOption(),
]);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
for ($index = \count($tokens) - 1; 0 <= $index; --$index) {
$token = $tokens[$index];
$this->fixElseif($index, $token, $tokens);
$this->fixElse($index, $token, $tokens);
$this->fixOpenCloseControls($index, $token, $tokens);
}
}
private function findParenthesisEnd(Tokens $tokens, int $structureTokenIndex): int
{
$nextIndex = $tokens->getNextMeaningfulToken($structureTokenIndex);
$nextToken = $tokens[$nextIndex];
return $nextToken->equals('(')
? $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $nextIndex)
: $structureTokenIndex; // return if next token is not opening parenthesis
}
/**
* Handle both extremes of the control structures.
* e.g. if(): or endif;.
*
* @param int $index the index of the token being processed
* @param Token $token the token being processed
* @param Tokens $tokens the collection of tokens
*/
private function fixOpenCloseControls(int $index, Token $token, Tokens $tokens): void
{
if ($token->isGivenKind([\T_IF, \T_FOREACH, \T_WHILE, \T_FOR, \T_SWITCH, \T_DECLARE])) {
$openIndex = $tokens->getNextTokenOfKind($index, ['(']);
$closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openIndex);
$afterParenthesisIndex = $tokens->getNextMeaningfulToken($closeIndex);
$afterParenthesis = $tokens[$afterParenthesisIndex];
if (!$afterParenthesis->equals(':')) {
return;
}
$items = [];
if (!$tokens[$afterParenthesisIndex - 1]->isWhitespace()) {
$items[] = new Token([\T_WHITESPACE, ' ']);
}
$items[] = new Token('{');
if (!$tokens[$afterParenthesisIndex + 1]->isWhitespace()) {
$items[] = new Token([\T_WHITESPACE, ' ']);
}
$tokens->clearAt($afterParenthesisIndex);
$tokens->insertAt($afterParenthesisIndex, $items);
}
if (!$token->isGivenKind([\T_ENDIF, \T_ENDFOREACH, \T_ENDWHILE, \T_ENDFOR, \T_ENDSWITCH, \T_ENDDECLARE])) {
return;
}
$nextTokenIndex = $tokens->getNextMeaningfulToken($index);
$nextToken = $tokens[$nextTokenIndex];
$tokens[$index] = new Token('}');
if ($nextToken->equals(';')) {
$tokens->clearAt($nextTokenIndex);
}
}
/**
* Handle the else: cases.
*
* @param int $index the index of the token being processed
* @param Token $token the token being processed
* @param Tokens $tokens the collection of tokens
*/
private function fixElse(int $index, Token $token, Tokens $tokens): void
{
if (!$token->isGivenKind(\T_ELSE)) {
return;
}
$tokenAfterElseIndex = $tokens->getNextMeaningfulToken($index);
$tokenAfterElse = $tokens[$tokenAfterElseIndex];
if (!$tokenAfterElse->equals(':')) {
return;
}
$this->addBraces($tokens, new Token([\T_ELSE, 'else']), $index, $tokenAfterElseIndex);
}
/**
* Handle the elsif(): cases.
*
* @param int $index the index of the token being processed
* @param Token $token the token being processed
* @param Tokens $tokens the collection of tokens
*/
private function fixElseif(int $index, Token $token, Tokens $tokens): void
{
if (!$token->isGivenKind(\T_ELSEIF)) {
return;
}
$parenthesisEndIndex = $this->findParenthesisEnd($tokens, $index);
$tokenAfterParenthesisIndex = $tokens->getNextMeaningfulToken($parenthesisEndIndex);
$tokenAfterParenthesis = $tokens[$tokenAfterParenthesisIndex];
if (!$tokenAfterParenthesis->equals(':')) {
return;
}
$this->addBraces($tokens, new Token([\T_ELSEIF, 'elseif']), $index, $tokenAfterParenthesisIndex);
}
/**
* Add opening and closing braces to the else: and elseif: cases.
*
* @param Tokens $tokens the tokens collection
* @param Token $token the current token
* @param int $index the current token index
* @param int $colonIndex the index of the colon
*/
private function addBraces(Tokens $tokens, Token $token, int $index, int $colonIndex): void
{
$items = [
new Token('}'),
new Token([\T_WHITESPACE, ' ']),
$token,
];
if (!$tokens[$index + 1]->isWhitespace()) {
$items[] = new Token([\T_WHITESPACE, ' ']);
}
$tokens->clearAt($index);
$tokens->insertAt(
$index,
$items
);
// increment the position of the colon by number of items inserted
$colonIndex += \count($items);
$items = [new Token('{')];
if (!$tokens[$colonIndex + 1]->isWhitespace()) {
$items[] = new Token([\T_WHITESPACE, ' ']);
}
$tokens->clearAt($colonIndex);
$tokens->insertAt(
$colonIndex,
$items
);
}
}

View File

@@ -0,0 +1,356 @@
<?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\ControlStructure;
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\Preg;
use PhpCsFixer\Tokenizer\Analyzer\WhitespacesAnalyzer;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
use Symfony\Component\OptionsResolver\Options;
/**
* Fixer for rule defined in PSR2 ¶5.2.
*
* @phpstan-type _AutogeneratedInputConfiguration array{
* comment_text?: string,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* comment_text: string,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*/
final class NoBreakCommentFixer extends AbstractFixer implements ConfigurableFixerInterface, WhitespacesAwareFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
private const STRUCTURE_KINDS = [\T_FOR, \T_FOREACH, \T_WHILE, \T_IF, \T_ELSEIF, \T_SWITCH, \T_FUNCTION, FCT::T_MATCH];
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'There must be a comment when fall-through is intentional in a non-empty case body.',
[
new CodeSample(
'<?php
switch ($foo) {
case 1:
foo();
case 2:
bar();
// no break
break;
case 3:
baz();
}
'
),
new CodeSample(
'<?php
switch ($foo) {
case 1:
foo();
case 2:
foo();
}
',
['comment_text' => 'some comment']
),
],
'Adds a "no break" comment before fall-through cases, and removes it if there is no fall-through.'
);
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(\T_SWITCH);
}
/**
* {@inheritdoc}
*
* Must run after NoUselessElseFixer.
*/
public function getPriority(): int
{
return 0;
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('comment_text', 'The text to use in the added comment and to detect it.'))
->setAllowedTypes(['string'])
->setAllowedValues([
static function (string $value): bool {
if (Preg::match('/\R/', $value)) {
throw new InvalidOptionsException('The comment text must not contain new lines.');
}
return true;
},
])
->setNormalizer(static fn (Options $options, string $value): string => rtrim($value))
->setDefault('no break')
->getOption(),
]);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
for ($index = \count($tokens) - 1; $index >= 0; --$index) {
if ($tokens[$index]->isGivenKind(\T_DEFAULT)) {
if ($tokens[$tokens->getNextMeaningfulToken($index)]->isGivenKind(\T_DOUBLE_ARROW)) {
continue; // this is "default" from "match"
}
} elseif (!$tokens[$index]->isGivenKind(\T_CASE)) {
continue;
}
$this->fixCase($tokens, $tokens->getNextTokenOfKind($index, [':', ';']));
}
}
private function fixCase(Tokens $tokens, int $casePosition): void
{
$empty = true;
$fallThrough = true;
$commentPosition = null;
for ($i = $casePosition + 1, $max = \count($tokens); $i < $max; ++$i) {
if ($tokens[$i]->isGivenKind([...self::STRUCTURE_KINDS, \T_ELSE, \T_DO, \T_CLASS])) {
$empty = false;
$i = $this->getStructureEnd($tokens, $i);
continue;
}
if ($tokens[$i]->isGivenKind([\T_BREAK, \T_CONTINUE, \T_RETURN, \T_EXIT, \T_GOTO])) {
$fallThrough = false;
continue;
}
if ($tokens[$i]->isGivenKind(\T_THROW)) {
$previousIndex = $tokens->getPrevMeaningfulToken($i);
if ($previousIndex === $casePosition || $tokens[$previousIndex]->equalsAny(['{', ';', '}', [\T_OPEN_TAG]])) {
$fallThrough = false;
}
continue;
}
if ($tokens[$i]->equals('}') || $tokens[$i]->isGivenKind(\T_ENDSWITCH)) {
if (null !== $commentPosition) {
$this->removeComment($tokens, $commentPosition);
}
break;
}
if ($this->isNoBreakComment($tokens[$i])) {
$commentPosition = $i;
continue;
}
if ($tokens[$i]->isGivenKind([\T_CASE, \T_DEFAULT])) {
if (!$empty && $fallThrough) {
if (null !== $commentPosition && $tokens->getPrevNonWhitespace($i) !== $commentPosition) {
$this->removeComment($tokens, $commentPosition);
$commentPosition = null;
}
if (null === $commentPosition) {
$this->insertCommentAt($tokens, $i);
} else {
$text = $this->configuration['comment_text'];
$tokens[$commentPosition] = new Token([
$tokens[$commentPosition]->getId(),
str_ireplace($text, $text, $tokens[$commentPosition]->getContent()),
]);
$this->ensureNewLineAt($tokens, $commentPosition);
}
} elseif (null !== $commentPosition) {
$this->removeComment($tokens, $commentPosition);
}
break;
}
if (!$tokens[$i]->isGivenKind([\T_COMMENT, \T_WHITESPACE])) {
$empty = false;
}
}
}
private function isNoBreakComment(Token $token): bool
{
if (!$token->isComment()) {
return false;
}
$text = preg_quote($this->configuration['comment_text'], '~');
return Preg::match("~^((//|#)\\s*{$text}\\s*)|(/\\*\\*?\\s*{$text}(\\s+.*)*\\*/)$~i", $token->getContent());
}
private function insertCommentAt(Tokens $tokens, int $casePosition): void
{
$lineEnding = $this->whitespacesConfig->getLineEnding();
$newlinePosition = $this->ensureNewLineAt($tokens, $casePosition);
$newlineToken = $tokens[$newlinePosition];
$nbNewlines = substr_count($newlineToken->getContent(), $lineEnding);
if ($newlineToken->isGivenKind(\T_OPEN_TAG) && Preg::match('/\R/', $newlineToken->getContent())) {
++$nbNewlines;
} elseif ($tokens[$newlinePosition - 1]->isGivenKind(\T_OPEN_TAG) && Preg::match('/\R/', $tokens[$newlinePosition - 1]->getContent())) {
++$nbNewlines;
if (!Preg::match('/\R/', $newlineToken->getContent())) {
$tokens[$newlinePosition] = new Token([$newlineToken->getId(), $lineEnding.$newlineToken->getContent()]);
}
}
if ($nbNewlines > 1) {
Preg::match('/^(.*?)(\R\h*)$/s', $newlineToken->getContent(), $matches);
$indent = WhitespacesAnalyzer::detectIndent($tokens, $newlinePosition - 1);
$tokens[$newlinePosition] = new Token([$newlineToken->getId(), $matches[1].$lineEnding.$indent]);
$tokens->insertAt(++$newlinePosition, new Token([\T_WHITESPACE, $matches[2]]));
}
$tokens->insertAt($newlinePosition, new Token([\T_COMMENT, '// '.$this->configuration['comment_text']]));
$this->ensureNewLineAt($tokens, $newlinePosition);
}
/**
* @return int The newline token position
*/
private function ensureNewLineAt(Tokens $tokens, int $position): int
{
$lineEnding = $this->whitespacesConfig->getLineEnding();
$content = $lineEnding.WhitespacesAnalyzer::detectIndent($tokens, $position);
$whitespaceToken = $tokens[$position - 1];
if (!$whitespaceToken->isGivenKind(\T_WHITESPACE)) {
if ($whitespaceToken->isGivenKind(\T_OPEN_TAG)) {
$content = Preg::replace('/\R/', '', $content);
if (!Preg::match('/\R/', $whitespaceToken->getContent())) {
$tokens[$position - 1] = new Token([\T_OPEN_TAG, Preg::replace('/\s+$/', $lineEnding, $whitespaceToken->getContent())]);
}
}
if ('' !== $content) {
$tokens->insertAt($position, new Token([\T_WHITESPACE, $content]));
return $position;
}
return $position - 1;
}
if ($tokens[$position - 2]->isGivenKind(\T_OPEN_TAG) && Preg::match('/\R/', $tokens[$position - 2]->getContent())) {
$content = Preg::replace('/^\R/', '', $content);
}
if (!Preg::match('/\R/', $whitespaceToken->getContent())) {
$tokens[$position - 1] = new Token([\T_WHITESPACE, $content]);
}
return $position - 1;
}
private function removeComment(Tokens $tokens, int $commentPosition): void
{
if ($tokens[$tokens->getPrevNonWhitespace($commentPosition)]->isGivenKind(\T_OPEN_TAG)) {
$whitespacePosition = $commentPosition + 1;
$regex = '/^\R\h*/';
} else {
$whitespacePosition = $commentPosition - 1;
$regex = '/\R\h*$/';
}
$whitespaceToken = $tokens[$whitespacePosition];
if ($whitespaceToken->isGivenKind(\T_WHITESPACE)) {
$content = Preg::replace($regex, '', $whitespaceToken->getContent());
$tokens->ensureWhitespaceAtIndex($whitespacePosition, 0, $content);
}
$tokens->clearTokenAndMergeSurroundingWhitespace($commentPosition);
}
private function getStructureEnd(Tokens $tokens, int $position): int
{
$initialToken = $tokens[$position];
if ($initialToken->isGivenKind(self::STRUCTURE_KINDS)) {
$position = $tokens->findBlockEnd(
Tokens::BLOCK_TYPE_PARENTHESIS_BRACE,
$tokens->getNextTokenOfKind($position, ['('])
);
} elseif ($initialToken->isGivenKind(\T_CLASS)) {
$openParenthesisPosition = $tokens->getNextMeaningfulToken($position);
if ('(' === $tokens[$openParenthesisPosition]->getContent()) {
$position = $tokens->findBlockEnd(
Tokens::BLOCK_TYPE_PARENTHESIS_BRACE,
$openParenthesisPosition
);
}
}
if ($initialToken->isGivenKind(\T_FUNCTION)) {
$position = $tokens->getNextTokenOfKind($position, ['{']);
} else {
$position = $tokens->getNextMeaningfulToken($position);
}
if ('{' !== $tokens[$position]->getContent()) {
return $tokens->getNextTokenOfKind($position, [';']);
}
$position = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $position);
if ($initialToken->isGivenKind(\T_DO)) {
$position = $tokens->findBlockEnd(
Tokens::BLOCK_TYPE_PARENTHESIS_BRACE,
$tokens->getNextTokenOfKind($position, ['('])
);
return $tokens->getNextTokenOfKind($position, [';']);
}
return $position;
}
}

View File

@@ -0,0 +1,100 @@
<?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\ControlStructure;
use PhpCsFixer\AbstractNoUselessElseFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
final class NoSuperfluousElseifFixer extends AbstractNoUselessElseFixer
{
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound([\T_ELSE, \T_ELSEIF]);
}
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Replaces superfluous `elseif` with `if`.',
[
new CodeSample("<?php\nif (\$a) {\n return 1;\n} elseif (\$b) {\n return 2;\n}\n"),
]
);
}
/**
* {@inheritdoc}
*
* Must run before SimplifiedIfReturnFixer.
* Must run after NoAlternativeSyntaxFixer.
*/
public function getPriority(): int
{
return parent::getPriority();
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
foreach ($tokens as $index => $token) {
if ($this->isElseif($tokens, $index) && $this->isSuperfluousElse($tokens, $index)) {
$this->convertElseifToIf($tokens, $index);
}
}
}
private function isElseif(Tokens $tokens, int $index): bool
{
return
$tokens[$index]->isGivenKind(\T_ELSEIF)
|| ($tokens[$index]->isGivenKind(\T_ELSE) && $tokens[$tokens->getNextMeaningfulToken($index)]->isGivenKind(\T_IF));
}
private function convertElseifToIf(Tokens $tokens, int $index): void
{
if ($tokens[$index]->isGivenKind(\T_ELSE)) {
$tokens->clearTokenAndMergeSurroundingWhitespace($index);
} else {
$tokens[$index] = new Token([\T_IF, 'if']);
}
$whitespace = '';
for ($previous = $index - 1; $previous > 0; --$previous) {
$token = $tokens[$previous];
if ($token->isWhitespace() && Preg::match('/(\R\N*)$/', $token->getContent(), $matches)) {
$whitespace = $matches[1];
break;
}
}
if ('' === $whitespace) {
return;
}
$previousToken = $tokens[$index - 1];
if (!$previousToken->isWhitespace()) {
$tokens->insertAt($index, new Token([\T_WHITESPACE, $whitespace]));
} elseif (!Preg::match('/\R/', $previousToken->getContent())) {
$tokens[$index - 1] = new Token([\T_WHITESPACE, $whitespace]);
}
}
}

View File

@@ -0,0 +1,51 @@
<?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\ControlStructure;
use PhpCsFixer\AbstractProxyFixer;
use PhpCsFixer\Fixer\Basic\NoTrailingCommaInSinglelineFixer;
use PhpCsFixer\Fixer\DeprecatedFixerInterface;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
/**
* @deprecated
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class NoTrailingCommaInListCallFixer extends AbstractProxyFixer implements DeprecatedFixerInterface
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Remove trailing commas in list function calls.',
[new CodeSample("<?php\nlist(\$a, \$b,) = foo();\n")]
);
}
public function getSuccessorsNames(): array
{
return array_keys($this->proxyFixers);
}
protected function createProxyFixers(): array
{
$fixer = new NoTrailingCommaInSinglelineFixer();
$fixer->configure(['elements' => ['array_destructuring']]);
return [$fixer];
}
}

View File

@@ -0,0 +1,180 @@
<?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\ControlStructure;
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{
* namespaces?: bool,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* namespaces: bool,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*/
final class NoUnneededBracesFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Removes unneeded braces that are superfluous and aren\'t part of a control structure\'s body.',
[
new CodeSample(
'<?php {
echo 1;
}
switch ($b) {
case 1: {
break;
}
}
'
),
new CodeSample(
'<?php
namespace Foo {
function Bar(){}
}
',
['namespaces' => true]
),
]
);
}
/**
* {@inheritdoc}
*
* Must run before NoUselessElseFixer, NoUselessReturnFixer, ReturnAssignmentFixer, SimplifiedIfReturnFixer.
*/
public function getPriority(): int
{
return 40;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound('}');
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
foreach ($this->findBraceOpen($tokens) as $index) {
if ($this->isOverComplete($tokens, $index)) {
$this->clearOverCompleteBraces($tokens, $index, $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index));
}
}
if (true === $this->configuration['namespaces']) {
$this->clearIfIsOverCompleteNamespaceBlock($tokens);
}
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('namespaces', 'Remove unneeded braces from bracketed namespaces.'))
->setAllowedTypes(['bool'])
->setDefault(false)
->getOption(),
]);
}
/**
* @param int $openIndex index of `{` token
* @param int $closeIndex index of `}` token
*/
private function clearOverCompleteBraces(Tokens $tokens, int $openIndex, int $closeIndex): void
{
$tokens->clearTokenAndMergeSurroundingWhitespace($closeIndex);
$tokens->clearTokenAndMergeSurroundingWhitespace($openIndex);
}
/**
* @return iterable<int>
*/
private function findBraceOpen(Tokens $tokens): iterable
{
for ($i = \count($tokens) - 1; $i > 0; --$i) {
if ($tokens[$i]->equals('{')) {
yield $i;
}
}
}
/**
* @param int $index index of `{` token
*/
private function isOverComplete(Tokens $tokens, int $index): bool
{
return $tokens[$tokens->getPrevMeaningfulToken($index)]->equalsAny(['{', '}', [\T_OPEN_TAG], ':', ';']);
}
private function clearIfIsOverCompleteNamespaceBlock(Tokens $tokens): void
{
if (1 !== $tokens->countTokenKind(\T_NAMESPACE)) {
return; // fast check, we never fix if multiple namespaces are defined
}
$index = $tokens->getNextTokenOfKind(0, [[\T_NAMESPACE]]);
$namespaceIsNamed = false;
$index = $tokens->getNextMeaningfulToken($index);
while ($tokens[$index]->isGivenKind([\T_STRING, \T_NS_SEPARATOR])) {
$index = $tokens->getNextMeaningfulToken($index);
$namespaceIsNamed = true;
}
if (!$namespaceIsNamed) {
return;
}
if (!$tokens[$index]->equals('{')) {
return; // `;`
}
$closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
$afterCloseIndex = $tokens->getNextMeaningfulToken($closeIndex);
if (null !== $afterCloseIndex && (!$tokens[$afterCloseIndex]->isGivenKind(\T_CLOSE_TAG) || null !== $tokens->getNextMeaningfulToken($afterCloseIndex))) {
return;
}
// clear up
$tokens->clearTokenAndMergeSurroundingWhitespace($closeIndex);
$tokens[$index] = new Token(';');
if ($tokens[$index - 1]->isWhitespace(" \t") && !$tokens[$index - 2]->isComment()) {
$tokens->clearTokenAndMergeSurroundingWhitespace($index - 1);
}
}
}

View File

@@ -0,0 +1,745 @@
<?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\ControlStructure;
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\Tokenizer\CT;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
/**
* @phpstan-type _AutogeneratedInputConfiguration array{
* statements?: list<'break'|'clone'|'continue'|'echo_print'|'negative_instanceof'|'others'|'return'|'switch_case'|'yield'|'yield_from'>,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* statements: list<'break'|'clone'|'continue'|'echo_print'|'negative_instanceof'|'others'|'return'|'switch_case'|'yield'|'yield_from'>,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Sullivan Senechal <soullivaneuh@gmail.com>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
* @author Gregor Harlan <gharlan@web.de>
*/
final class NoUnneededControlParenthesesFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
/**
* @var list<int>
*/
private const BLOCK_TYPES = [
Tokens::BLOCK_TYPE_ARRAY_INDEX_CURLY_BRACE,
Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE,
Tokens::BLOCK_TYPE_CURLY_BRACE,
Tokens::BLOCK_TYPE_DESTRUCTURING_SQUARE_BRACE,
Tokens::BLOCK_TYPE_DYNAMIC_PROP_BRACE,
Tokens::BLOCK_TYPE_DYNAMIC_VAR_BRACE,
Tokens::BLOCK_TYPE_INDEX_SQUARE_BRACE,
Tokens::BLOCK_TYPE_PARENTHESIS_BRACE,
];
private const BEFORE_TYPES = [
';',
'{',
[\T_OPEN_TAG],
[\T_OPEN_TAG_WITH_ECHO],
[\T_ECHO],
[\T_PRINT],
[\T_RETURN],
[\T_THROW],
[\T_YIELD],
[\T_YIELD_FROM],
[\T_BREAK],
[\T_CONTINUE],
// won't be fixed, but true in concept, helpful for fast check
[\T_REQUIRE],
[\T_REQUIRE_ONCE],
[\T_INCLUDE],
[\T_INCLUDE_ONCE],
];
private const CONFIG_OPTIONS = [
'break',
'clone',
'continue',
'echo_print',
'negative_instanceof',
'others',
'return',
'switch_case',
'yield',
'yield_from',
];
private const TOKEN_TYPE_CONFIG_MAP = [
\T_BREAK => 'break',
\T_CASE => 'switch_case',
\T_CONTINUE => 'continue',
\T_ECHO => 'echo_print',
\T_PRINT => 'echo_print',
\T_RETURN => 'return',
\T_YIELD => 'yield',
\T_YIELD_FROM => 'yield_from',
];
// handled by the `include` rule
private const TOKEN_TYPE_NO_CONFIG = [
\T_REQUIRE,
\T_REQUIRE_ONCE,
\T_INCLUDE,
\T_INCLUDE_ONCE,
];
private const KNOWN_NEGATIVE_PRE_TYPES = [
[CT::T_CLASS_CONSTANT],
[CT::T_DYNAMIC_VAR_BRACE_CLOSE],
[CT::T_RETURN_REF],
[CT::T_USE_LAMBDA],
[\T_ARRAY],
[\T_CATCH],
[\T_CLASS],
[\T_DECLARE],
[\T_ELSEIF],
[\T_EMPTY],
[\T_EXIT],
[\T_EVAL],
[\T_FN],
[\T_FOREACH],
[\T_FOR],
[\T_FUNCTION],
[\T_HALT_COMPILER],
[\T_IF],
[\T_ISSET],
[\T_LIST],
[\T_STRING],
[\T_SWITCH],
[\T_STATIC],
[\T_UNSET],
[\T_VARIABLE],
[\T_WHILE],
// handled by the `include` rule
[\T_REQUIRE],
[\T_REQUIRE_ONCE],
[\T_INCLUDE],
[\T_INCLUDE_ONCE],
[FCT::T_MATCH],
];
/**
* @var list<array{int}|string>
*/
private array $noopTypes;
private TokensAnalyzer $tokensAnalyzer;
public function __construct()
{
parent::__construct();
$this->noopTypes = [
'$',
[\T_CONSTANT_ENCAPSED_STRING],
[\T_DNUMBER],
[\T_DOUBLE_COLON],
[\T_LNUMBER],
[\T_NS_SEPARATOR],
[\T_STRING],
[\T_VARIABLE],
[\T_STATIC],
// magic constants
[\T_CLASS_C],
[\T_DIR],
[\T_FILE],
[\T_FUNC_C],
[\T_LINE],
[\T_METHOD_C],
[\T_NS_C],
[\T_TRAIT_C],
];
foreach (Token::getObjectOperatorKinds() as $kind) {
$this->noopTypes[] = [$kind];
}
}
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Removes unneeded parentheses around control statements.',
[
new CodeSample(
'<?php
while ($x) { while ($y) { break (2); } }
clone($a);
while ($y) { continue (2); }
echo("foo");
print("foo");
return (1 + 2);
switch ($a) { case($x); }
yield(2);
'
),
new CodeSample(
'<?php
while ($x) { while ($y) { break (2); } }
clone($a);
while ($y) { continue (2); }
',
['statements' => ['break', 'continue']]
),
]
);
}
/**
* {@inheritdoc}
*
* Must run before ConcatSpaceFixer, NewExpressionParenthesesFixer, NoTrailingWhitespaceFixer.
* Must run after ModernizeTypesCastingFixer, NoAlternativeSyntaxFixer.
*/
public function getPriority(): int
{
return 30;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound(['(', CT::T_BRACE_CLASS_INSTANTIATION_OPEN]);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$this->tokensAnalyzer = new TokensAnalyzer($tokens);
foreach ($tokens as $openIndex => $token) {
if ($token->equals('(')) {
$closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openIndex);
} elseif ($token->isGivenKind(CT::T_BRACE_CLASS_INSTANTIATION_OPEN)) {
$closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_BRACE_CLASS_INSTANTIATION, $openIndex);
} else {
continue;
}
$beforeOpenIndex = $tokens->getPrevMeaningfulToken($openIndex);
$afterCloseIndex = $tokens->getNextMeaningfulToken($closeIndex);
// do a cheap check for negative case: `X()`
if ($tokens->getNextMeaningfulToken($openIndex) === $closeIndex) {
if ($this->isExitStatement($tokens, $beforeOpenIndex)) {
$this->removeUselessParenthesisPair($tokens, $beforeOpenIndex, $afterCloseIndex, $openIndex, $closeIndex, 'others');
}
continue;
}
// do a cheap check for negative case: `foo(1,2)`
if ($tokens[$beforeOpenIndex]->equalsAny(self::KNOWN_NEGATIVE_PRE_TYPES)) {
continue;
}
// check for the simple useless wrapped cases
if ($this->isUselessWrapped($tokens, $beforeOpenIndex, $afterCloseIndex)) {
$this->removeUselessParenthesisPair($tokens, $beforeOpenIndex, $afterCloseIndex, $openIndex, $closeIndex, $this->getConfigType($tokens, $beforeOpenIndex));
continue;
}
// handle `clone` statements
if ($this->isCloneStatement($tokens, $beforeOpenIndex)) {
if ($this->isWrappedCloneArgument($tokens, $beforeOpenIndex, $openIndex, $closeIndex, $afterCloseIndex)) {
$this->removeUselessParenthesisPair($tokens, $beforeOpenIndex, $afterCloseIndex, $openIndex, $closeIndex, 'clone');
}
continue;
}
// handle `instance of` statements
$instanceOfIndex = $this->getIndexOfInstanceOfStatement($tokens, $openIndex, $closeIndex);
if (null !== $instanceOfIndex) {
if ($this->isWrappedInstanceOf($tokens, $instanceOfIndex, $beforeOpenIndex, $openIndex, $closeIndex, $afterCloseIndex)) {
$this->removeUselessParenthesisPair(
$tokens,
$beforeOpenIndex,
$afterCloseIndex,
$openIndex,
$closeIndex,
$tokens[$beforeOpenIndex]->equals('!') ? 'negative_instanceof' : 'others'
);
}
continue;
}
// last checks deal with operators, do not swap around
if ($this->isWrappedPartOfOperation($tokens, $beforeOpenIndex, $openIndex, $closeIndex, $afterCloseIndex)) {
$this->removeUselessParenthesisPair($tokens, $beforeOpenIndex, $afterCloseIndex, $openIndex, $closeIndex, $this->getConfigType($tokens, $beforeOpenIndex));
}
}
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
$defaults = array_filter(
self::CONFIG_OPTIONS,
static fn (string $option): bool => 'negative_instanceof' !== $option && 'others' !== $option && 'yield_from' !== $option
);
return new FixerConfigurationResolver([
(new FixerOptionBuilder('statements', 'List of control statements to fix.'))
->setAllowedTypes(['string[]'])
->setAllowedValues([new AllowedValueSubset(self::CONFIG_OPTIONS)])
->setDefault(array_values($defaults))
->getOption(),
]);
}
private function isUselessWrapped(Tokens $tokens, int $beforeOpenIndex, int $afterCloseIndex): bool
{
return
$this->isSingleStatement($tokens, $beforeOpenIndex, $afterCloseIndex)
|| $this->isWrappedFnBody($tokens, $beforeOpenIndex, $afterCloseIndex)
|| $this->isWrappedForElement($tokens, $beforeOpenIndex, $afterCloseIndex)
|| $this->isWrappedLanguageConstructArgument($tokens, $beforeOpenIndex, $afterCloseIndex)
|| $this->isWrappedSequenceElement($tokens, $beforeOpenIndex, $afterCloseIndex);
}
private function isExitStatement(Tokens $tokens, int $beforeOpenIndex): bool
{
return $tokens[$beforeOpenIndex]->isGivenKind(\T_EXIT);
}
private function isCloneStatement(Tokens $tokens, int $beforeOpenIndex): bool
{
return $tokens[$beforeOpenIndex]->isGivenKind(\T_CLONE);
}
private function isWrappedCloneArgument(Tokens $tokens, int $beforeOpenIndex, int $openIndex, int $closeIndex, int $afterCloseIndex): bool
{
$beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
if (
!(
$tokens[$beforeOpenIndex]->equals('?') // For BC reasons
|| $this->isSimpleAssignment($tokens, $beforeOpenIndex, $afterCloseIndex)
|| $this->isSingleStatement($tokens, $beforeOpenIndex, $afterCloseIndex)
|| $this->isWrappedFnBody($tokens, $beforeOpenIndex, $afterCloseIndex)
|| $this->isWrappedForElement($tokens, $beforeOpenIndex, $afterCloseIndex)
|| $this->isWrappedSequenceElement($tokens, $beforeOpenIndex, $afterCloseIndex)
)
) {
return false;
}
$newCandidateIndex = $tokens->getNextMeaningfulToken($openIndex);
if ($tokens[$newCandidateIndex]->isGivenKind(\T_NEW)) {
$openIndex = $newCandidateIndex; // `clone (new X)`, `clone (new X())`, clone (new X(Y))`
}
return !$this->containsOperation($tokens, $openIndex, $closeIndex);
}
private function getIndexOfInstanceOfStatement(Tokens $tokens, int $openIndex, int $closeIndex): ?int
{
$instanceOfIndex = $tokens->findGivenKind(\T_INSTANCEOF, $openIndex, $closeIndex);
return 1 === \count($instanceOfIndex) ? array_key_first($instanceOfIndex) : null;
}
private function isWrappedInstanceOf(Tokens $tokens, int $instanceOfIndex, int $beforeOpenIndex, int $openIndex, int $closeIndex, int $afterCloseIndex): bool
{
if (
$this->containsOperation($tokens, $openIndex, $instanceOfIndex)
|| $this->containsOperation($tokens, $instanceOfIndex, $closeIndex)
) {
return false;
}
if ($tokens[$beforeOpenIndex]->equals('!')) {
$beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
}
return
$this->isSimpleAssignment($tokens, $beforeOpenIndex, $afterCloseIndex)
|| $this->isSingleStatement($tokens, $beforeOpenIndex, $afterCloseIndex)
|| $this->isWrappedFnBody($tokens, $beforeOpenIndex, $afterCloseIndex)
|| $this->isWrappedForElement($tokens, $beforeOpenIndex, $afterCloseIndex)
|| $this->isWrappedSequenceElement($tokens, $beforeOpenIndex, $afterCloseIndex);
}
private function isWrappedPartOfOperation(Tokens $tokens, int $beforeOpenIndex, int $openIndex, int $closeIndex, int $afterCloseIndex): bool
{
if ($this->containsOperation($tokens, $openIndex, $closeIndex)) {
return false;
}
$boundariesMoved = false;
if ($this->isPreUnaryOperation($tokens, $beforeOpenIndex)) {
$beforeOpenIndex = $this->getBeforePreUnaryOperation($tokens, $beforeOpenIndex);
$boundariesMoved = true;
}
if ($this->isAccess($tokens, $afterCloseIndex)) {
$afterCloseIndex = $this->getAfterAccess($tokens, $afterCloseIndex);
$boundariesMoved = true;
if ($this->tokensAnalyzer->isUnarySuccessorOperator($afterCloseIndex)) { // post unary operation are only valid here
$afterCloseIndex = $tokens->getNextMeaningfulToken($afterCloseIndex);
}
}
if ($boundariesMoved) {
if ($tokens[$beforeOpenIndex]->equalsAny(self::KNOWN_NEGATIVE_PRE_TYPES)) {
return false;
}
if ($this->isUselessWrapped($tokens, $beforeOpenIndex, $afterCloseIndex)) {
return true;
}
}
// check if part of some operation sequence
$beforeIsBinaryOperation = $this->tokensAnalyzer->isBinaryOperator($beforeOpenIndex);
$afterIsBinaryOperation = $this->tokensAnalyzer->isBinaryOperator($afterCloseIndex);
if ($beforeIsBinaryOperation && $afterIsBinaryOperation) {
return true; // `+ (x) +`
}
$beforeToken = $tokens[$beforeOpenIndex];
$afterToken = $tokens[$afterCloseIndex];
$beforeIsBlockOpenOrComma = $beforeToken->equals(',') || null !== $this->getBlock($tokens, $beforeOpenIndex, true);
$afterIsBlockEndOrComma = $afterToken->equals(',') || null !== $this->getBlock($tokens, $afterCloseIndex, false);
if (($beforeIsBlockOpenOrComma && $afterIsBinaryOperation) || ($beforeIsBinaryOperation && $afterIsBlockEndOrComma)) {
// $beforeIsBlockOpenOrComma && $afterIsBlockEndOrComma is covered by `isWrappedSequenceElement`
// `[ (x) +` or `+ (X) ]` or `, (X) +` or `+ (X) ,`
return true;
}
if ($tokens[$beforeOpenIndex]->equals('}')) {
$beforeIsStatementOpen = !$this->closeCurlyBelongsToDynamicElement($tokens, $beforeOpenIndex);
} else {
$beforeIsStatementOpen = $beforeToken->equalsAny(self::BEFORE_TYPES) || $beforeToken->isGivenKind(\T_CASE);
}
$afterIsStatementEnd = $afterToken->equalsAny([';', [\T_CLOSE_TAG]]);
return
($beforeIsStatementOpen && $afterIsBinaryOperation) // `<?php (X) +`
|| ($beforeIsBinaryOperation && $afterIsStatementEnd); // `+ (X);`
}
// bounded `print|yield|yield from|require|require_once|include|include_once (X)`
private function isWrappedLanguageConstructArgument(Tokens $tokens, int $beforeOpenIndex, int $afterCloseIndex): bool
{
if (!$tokens[$beforeOpenIndex]->isGivenKind([\T_PRINT, \T_YIELD, \T_YIELD_FROM, \T_REQUIRE, \T_REQUIRE_ONCE, \T_INCLUDE, \T_INCLUDE_ONCE])) {
return false;
}
$beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
return $this->isWrappedSequenceElement($tokens, $beforeOpenIndex, $afterCloseIndex);
}
// any of `<?php|<?|<?=|;|throw|return|... (X) ;|T_CLOSE`
private function isSingleStatement(Tokens $tokens, int $beforeOpenIndex, int $afterCloseIndex): bool
{
if ($tokens[$beforeOpenIndex]->isGivenKind(\T_CASE)) {
return $tokens[$afterCloseIndex]->equalsAny([':', ';']); // `switch case`
}
if (!$tokens[$afterCloseIndex]->equalsAny([';', [\T_CLOSE_TAG]])) {
return false;
}
if ($tokens[$beforeOpenIndex]->equals('}')) {
return !$this->closeCurlyBelongsToDynamicElement($tokens, $beforeOpenIndex);
}
return $tokens[$beforeOpenIndex]->equalsAny(self::BEFORE_TYPES);
}
private function isSimpleAssignment(Tokens $tokens, int $beforeOpenIndex, int $afterCloseIndex): bool
{
return $tokens[$beforeOpenIndex]->equals('=') && $tokens[$afterCloseIndex]->equalsAny([';', [\T_CLOSE_TAG]]); // `= (X) ;`
}
private function isWrappedSequenceElement(Tokens $tokens, int $startIndex, int $endIndex): bool
{
$startIsComma = $tokens[$startIndex]->equals(',');
$endIsComma = $tokens[$endIndex]->equals(',');
if ($startIsComma && $endIsComma) {
return true; // `,(X),`
}
$blockTypeStart = $this->getBlock($tokens, $startIndex, true);
$blockTypeEnd = $this->getBlock($tokens, $endIndex, false);
return
($startIsComma && null !== $blockTypeEnd) // `,(X)]`
|| ($endIsComma && null !== $blockTypeStart) // `[(X),`
|| (null !== $blockTypeEnd && null !== $blockTypeStart); // any type of `{(X)}`, `[(X)]` and `((X))`
}
// any of `for( (X); ;(X)) ;` note that the middle element is covered as 'single statement' as it is `; (X) ;`
private function isWrappedForElement(Tokens $tokens, int $beforeOpenIndex, int $afterCloseIndex): bool
{
$forCandidateIndex = null;
if ($tokens[$beforeOpenIndex]->equals('(') && $tokens[$afterCloseIndex]->equals(';')) {
$forCandidateIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
} elseif ($tokens[$afterCloseIndex]->equals(')') && $tokens[$beforeOpenIndex]->equals(';')) {
$forCandidateIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $afterCloseIndex);
$forCandidateIndex = $tokens->getPrevMeaningfulToken($forCandidateIndex);
}
return null !== $forCandidateIndex && $tokens[$forCandidateIndex]->isGivenKind(\T_FOR);
}
// `fn() => (X);`
private function isWrappedFnBody(Tokens $tokens, int $beforeOpenIndex, int $afterCloseIndex): bool
{
if (!$tokens[$beforeOpenIndex]->isGivenKind(\T_DOUBLE_ARROW)) {
return false;
}
$beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
if ($tokens[$beforeOpenIndex]->isGivenKind(\T_STRING)) {
while (true) {
$beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
if (!$tokens[$beforeOpenIndex]->isGivenKind([\T_STRING, CT::T_TYPE_INTERSECTION, CT::T_TYPE_ALTERNATION])) {
break;
}
}
if (!$tokens[$beforeOpenIndex]->isGivenKind(CT::T_TYPE_COLON)) {
return false;
}
$beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
}
if (!$tokens[$beforeOpenIndex]->equals(')')) {
return false;
}
$beforeOpenIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $beforeOpenIndex);
$beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
if ($tokens[$beforeOpenIndex]->isGivenKind(CT::T_RETURN_REF)) {
$beforeOpenIndex = $tokens->getPrevMeaningfulToken($beforeOpenIndex);
}
if (!$tokens[$beforeOpenIndex]->isGivenKind(\T_FN)) {
return false;
}
return $tokens[$afterCloseIndex]->equalsAny([';', ',', [\T_CLOSE_TAG]]);
}
private function isPreUnaryOperation(Tokens $tokens, int $index): bool
{
return $this->tokensAnalyzer->isUnaryPredecessorOperator($index) || $tokens[$index]->isCast();
}
private function getBeforePreUnaryOperation(Tokens $tokens, int $index): int
{
do {
$index = $tokens->getPrevMeaningfulToken($index);
} while ($this->isPreUnaryOperation($tokens, $index));
return $index;
}
// array access `(X)[` or `(X){` or object access `(X)->` or `(X)?->`
private function isAccess(Tokens $tokens, int $index): bool
{
$token = $tokens[$index];
return $token->isObjectOperator() || $token->equals('[') || $token->isGivenKind(CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN);
}
private function getAfterAccess(Tokens $tokens, int $index): int
{
while (true) {
$block = $this->getBlock($tokens, $index, true);
if (null !== $block) {
$index = $tokens->findBlockEnd($block['type'], $index);
$index = $tokens->getNextMeaningfulToken($index);
continue;
}
if (
$tokens[$index]->isObjectOperator()
|| $tokens[$index]->equalsAny(['$', [\T_PAAMAYIM_NEKUDOTAYIM], [\T_STRING], [\T_VARIABLE]])
) {
$index = $tokens->getNextMeaningfulToken($index);
continue;
}
break;
}
return $index;
}
/**
* @return null|array{type: Tokens::BLOCK_TYPE_*, isStart: bool}
*/
private function getBlock(Tokens $tokens, int $index, bool $isStart): ?array
{
$block = Tokens::detectBlockType($tokens[$index]);
return null !== $block && $isStart === $block['isStart'] && \in_array($block['type'], self::BLOCK_TYPES, true) ? $block : null;
}
private function containsOperation(Tokens $tokens, int $startIndex, int $endIndex): bool
{
while (true) {
$startIndex = $tokens->getNextMeaningfulToken($startIndex);
if ($startIndex === $endIndex) {
break;
}
$block = Tokens::detectBlockType($tokens[$startIndex]);
if (null !== $block && $block['isStart']) {
$startIndex = $tokens->findBlockEnd($block['type'], $startIndex);
continue;
}
if (!$tokens[$startIndex]->equalsAny($this->noopTypes)) {
return true;
}
}
return false;
}
private function getConfigType(Tokens $tokens, int $beforeOpenIndex): ?string
{
if ($tokens[$beforeOpenIndex]->isGivenKind(self::TOKEN_TYPE_NO_CONFIG)) {
return null;
}
foreach (self::TOKEN_TYPE_CONFIG_MAP as $type => $configItem) {
if ($tokens[$beforeOpenIndex]->isGivenKind($type)) {
return $configItem;
}
}
return 'others';
}
private function removeUselessParenthesisPair(
Tokens $tokens,
int $beforeOpenIndex,
int $afterCloseIndex,
int $openIndex,
int $closeIndex,
?string $configType
): void {
$statements = $this->configuration['statements'];
if (null === $configType || !\in_array($configType, $statements, true)) {
return;
}
$needsSpaceAfter = !$this->isAccess($tokens, $afterCloseIndex)
&& !$tokens[$afterCloseIndex]->equalsAny([';', ',', [\T_CLOSE_TAG]])
&& null === $this->getBlock($tokens, $afterCloseIndex, false)
&& !($tokens[$afterCloseIndex]->equalsAny([':', ';']) && $tokens[$beforeOpenIndex]->isGivenKind(\T_CASE));
$needsSpaceBefore = !$this->isPreUnaryOperation($tokens, $beforeOpenIndex)
&& !$tokens[$beforeOpenIndex]->equalsAny(['}', [\T_EXIT], [\T_OPEN_TAG]])
&& null === $this->getBlock($tokens, $beforeOpenIndex, true);
$this->removeBrace($tokens, $closeIndex, $needsSpaceAfter);
$this->removeBrace($tokens, $openIndex, $needsSpaceBefore);
}
private function removeBrace(Tokens $tokens, int $index, bool $needsSpace): void
{
if ($needsSpace) {
foreach ([-1, 1] as $direction) {
$siblingIndex = $tokens->getNonEmptySibling($index, $direction);
if ($tokens[$siblingIndex]->isWhitespace() || $tokens[$siblingIndex]->isComment()) {
$needsSpace = false;
break;
}
}
}
if ($needsSpace) {
$tokens[$index] = new Token([\T_WHITESPACE, ' ']);
} else {
$tokens->clearTokenAndMergeSurroundingWhitespace($index);
}
}
private function closeCurlyBelongsToDynamicElement(Tokens $tokens, int $beforeOpenIndex): bool
{
$index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $beforeOpenIndex);
$index = $tokens->getPrevMeaningfulToken($index);
if ($tokens[$index]->isGivenKind(\T_DOUBLE_COLON)) {
return true;
}
if ($tokens[$index]->equals(':')) {
$index = $tokens->getPrevTokenOfKind($index, [[\T_CASE], '?']);
return !$tokens[$index]->isGivenKind(\T_CASE);
}
return false;
}
}

View File

@@ -0,0 +1,106 @@
<?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\ControlStructure;
use PhpCsFixer\AbstractProxyFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
use PhpCsFixer\Fixer\DeprecatedFixerInterface;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
/**
* @deprecated
*
* @phpstan-type _AutogeneratedInputConfiguration array{
* namespaces?: bool,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* namespaces: bool,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*/
final class NoUnneededCurlyBracesFixer extends AbstractProxyFixer implements ConfigurableFixerInterface, DeprecatedFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
private NoUnneededBracesFixer $noUnneededBracesFixer;
public function __construct()
{
$this->noUnneededBracesFixer = new NoUnneededBracesFixer();
parent::__construct();
}
public function getDefinition(): FixerDefinitionInterface
{
$fixerDefinition = $this->noUnneededBracesFixer->getDefinition();
return new FixerDefinition(
'Removes unneeded curly braces that are superfluous and aren\'t part of a control structure\'s body.',
$fixerDefinition->getCodeSamples(),
$fixerDefinition->getDescription(),
$fixerDefinition->getRiskyDescription()
);
}
/**
* {@inheritdoc}
*
* Must run before NoUselessElseFixer, NoUselessReturnFixer, ReturnAssignmentFixer, SimplifiedIfReturnFixer.
*/
public function getPriority(): int
{
return $this->noUnneededBracesFixer->getPriority();
}
public function getSuccessorsNames(): array
{
return [
$this->noUnneededBracesFixer->getName(),
];
}
/**
* @param _AutogeneratedInputConfiguration $configuration
*/
protected function configurePreNormalisation(array $configuration): void
{
$this->noUnneededBracesFixer->configure($configuration);
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('namespaces', 'Remove unneeded curly braces from bracketed namespaces.'))
->setAllowedTypes(['bool'])
->setDefault(false)
->getOption(),
]);
}
protected function createProxyFixers(): array
{
return [
$this->noUnneededBracesFixer,
];
}
}

View File

@@ -0,0 +1,120 @@
<?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\ControlStructure;
use PhpCsFixer\AbstractNoUselessElseFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Tokens;
final class NoUselessElseFixer extends AbstractNoUselessElseFixer
{
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(\T_ELSE);
}
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'There should not be useless `else` cases.',
[
new CodeSample("<?php\nif (\$a) {\n return 1;\n} else {\n return 2;\n}\n"),
]
);
}
/**
* {@inheritdoc}
*
* Must run before BlankLineBeforeStatementFixer, BracesFixer, CombineConsecutiveUnsetsFixer, NoBreakCommentFixer, NoExtraBlankLinesFixer, NoTrailingWhitespaceFixer, NoUselessReturnFixer, NoWhitespaceInBlankLineFixer, SimplifiedIfReturnFixer, StatementIndentationFixer.
* Must run after NoAlternativeSyntaxFixer, NoEmptyStatementFixer, NoUnneededBracesFixer, NoUnneededCurlyBracesFixer.
*/
public function getPriority(): int
{
return parent::getPriority();
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
foreach ($tokens as $index => $token) {
if (!$token->isGivenKind(\T_ELSE)) {
continue;
}
// `else if` vs. `else` and alternative syntax `else:` checks
if ($tokens[$tokens->getNextMeaningfulToken($index)]->equalsAny([':', [\T_IF]])) {
continue;
}
// clean up `else` if it is an empty statement
$this->fixEmptyElse($tokens, $index);
if ($tokens->isEmptyAt($index)) {
continue;
}
// clean up `else` if possible
if ($this->isSuperfluousElse($tokens, $index)) {
$this->clearElse($tokens, $index);
}
}
}
/**
* Remove tokens part of an `else` statement if not empty (i.e. no meaningful tokens inside).
*
* @param int $index T_ELSE index
*/
private function fixEmptyElse(Tokens $tokens, int $index): void
{
$next = $tokens->getNextMeaningfulToken($index);
if ($tokens[$next]->equals('{')) {
$close = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $next);
if (1 === $close - $next) { // '{}'
$this->clearElse($tokens, $index);
} elseif ($tokens->getNextMeaningfulToken($next) === $close) { // '{/**/}'
$this->clearElse($tokens, $index);
}
return;
}
// short `else`
$end = $tokens->getNextTokenOfKind($index, [';', [\T_CLOSE_TAG]]);
if ($next === $end) {
$this->clearElse($tokens, $index);
}
}
/**
* @param int $index index of T_ELSE
*/
private function clearElse(Tokens $tokens, int $index): void
{
$tokens->clearTokenAndMergeSurroundingWhitespace($index);
// clear T_ELSE and the '{' '}' if there are any
$next = $tokens->getNextMeaningfulToken($index);
if (!$tokens[$next]->equals('{')) {
return;
}
$tokens->clearTokenAndMergeSurroundingWhitespace($tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $next));
$tokens->clearTokenAndMergeSurroundingWhitespace($next);
}
}

View File

@@ -0,0 +1,138 @@
<?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\ControlStructure;
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 SimplifiedIfReturnFixer extends AbstractFixer
{
/**
* @var list<array{isNegative: bool, sequence: non-empty-list<array{0: int, 1?: string}|string>}>
*/
private array $sequences = [
[
'isNegative' => false,
'sequence' => [
'{', [\T_RETURN], [\T_STRING, 'true'], ';', '}',
[\T_RETURN], [\T_STRING, 'false'], ';',
],
],
[
'isNegative' => true,
'sequence' => [
'{', [\T_RETURN], [\T_STRING, 'false'], ';', '}',
[\T_RETURN], [\T_STRING, 'true'], ';',
],
],
[
'isNegative' => false,
'sequence' => [
[\T_RETURN], [\T_STRING, 'true'], ';',
[\T_RETURN], [\T_STRING, 'false'], ';',
],
],
[
'isNegative' => true,
'sequence' => [
[\T_RETURN], [\T_STRING, 'false'], ';',
[\T_RETURN], [\T_STRING, 'true'], ';',
],
],
];
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Simplify `if` control structures that return the boolean result of their condition.',
[new CodeSample("<?php\nif (\$foo) { return true; } return false;\n")]
);
}
/**
* {@inheritdoc}
*
* Must run before MultilineWhitespaceBeforeSemicolonsFixer, NoSinglelineWhitespaceBeforeSemicolonsFixer.
* Must run after NoSuperfluousElseifFixer, NoUnneededBracesFixer, NoUnneededCurlyBracesFixer, NoUselessElseFixer, SemicolonAfterInstructionFixer.
*/
public function getPriority(): int
{
return 1;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAllTokenKindsFound([\T_IF, \T_RETURN, \T_STRING]);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
for ($ifIndex = $tokens->count() - 1; 0 <= $ifIndex; --$ifIndex) {
if (!$tokens[$ifIndex]->isGivenKind([\T_IF, \T_ELSEIF])) {
continue;
}
if ($tokens[$tokens->getPrevMeaningfulToken($ifIndex)]->equals(')')) {
continue; // in a loop without braces
}
$startParenthesisIndex = $tokens->getNextTokenOfKind($ifIndex, ['(']);
$endParenthesisIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $startParenthesisIndex);
$firstCandidateIndex = $tokens->getNextMeaningfulToken($endParenthesisIndex);
foreach ($this->sequences as $sequenceSpec) {
$sequenceFound = $tokens->findSequence($sequenceSpec['sequence'], $firstCandidateIndex);
if (null === $sequenceFound) {
continue;
}
$firstSequenceIndex = array_key_first($sequenceFound);
if ($firstSequenceIndex !== $firstCandidateIndex) {
continue;
}
$indicesToClear = array_keys($sequenceFound);
array_pop($indicesToClear); // Preserve last semicolon
rsort($indicesToClear);
foreach ($indicesToClear as $index) {
$tokens->clearTokenAndMergeSurroundingWhitespace($index);
}
$newTokens = [
new Token([\T_RETURN, 'return']),
new Token([\T_WHITESPACE, ' ']),
];
if ($sequenceSpec['isNegative']) {
$newTokens[] = new Token('!');
} else {
$newTokens[] = new Token([\T_BOOL_CAST, '(bool)']);
}
$tokens->overrideRange($ifIndex, $ifIndex, $newTokens);
}
}
}
}

View File

@@ -0,0 +1,87 @@
<?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\ControlStructure;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\SwitchAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\ControlCaseStructuresAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* Fixer for rules defined in PSR2 ¶5.2.
*/
final class SwitchCaseSemicolonToColonFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'A case should be followed by a colon and not a semicolon.',
[
new CodeSample(
'<?php
switch ($a) {
case 1;
break;
default;
break;
}
'
),
]
);
}
/**
* {@inheritdoc}
*
* Must run after NoEmptyStatementFixer.
*/
public function getPriority(): int
{
return 0;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(\T_SWITCH);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
/** @var SwitchAnalysis $analysis */
foreach (ControlCaseStructuresAnalyzer::findControlStructures($tokens, [\T_SWITCH]) as $analysis) {
$default = $analysis->getDefaultAnalysis();
if (null !== $default) {
$this->fixTokenIfNeeded($tokens, $default->getColonIndex());
}
foreach ($analysis->getCases() as $caseAnalysis) {
$this->fixTokenIfNeeded($tokens, $caseAnalysis->getColonIndex());
}
}
}
private function fixTokenIfNeeded(Tokens $tokens, int $index): void
{
if ($tokens[$index]->equals(';')) {
$tokens[$index] = new Token(':');
}
}
}

View File

@@ -0,0 +1,85 @@
<?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\ControlStructure;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\SwitchAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\ControlCaseStructuresAnalyzer;
use PhpCsFixer\Tokenizer\Tokens;
/**
* Fixer for rules defined in PSR2 ¶5.2.
*
* @author Sullivan Senechal <soullivaneuh@gmail.com>
*/
final class SwitchCaseSpaceFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Removes extra spaces between colon and case value.',
[
new CodeSample(
'<?php
switch($a) {
case 1 :
break;
default :
return 2;
}
'
),
]
);
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(\T_SWITCH);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
/** @var SwitchAnalysis $analysis */
foreach (ControlCaseStructuresAnalyzer::findControlStructures($tokens, [\T_SWITCH]) as $analysis) {
$default = $analysis->getDefaultAnalysis();
if (null !== $default) {
$index = $default->getIndex();
if (!$tokens[$index + 1]->isWhitespace() || !$tokens[$index + 2]->equalsAny([':', ';'])) {
continue;
}
$tokens->clearAt($index + 1);
}
foreach ($analysis->getCases() as $caseAnalysis) {
$colonIndex = $caseAnalysis->getColonIndex();
$valueIndex = $tokens->getPrevNonWhitespace($colonIndex);
// skip if there is no space between the colon and previous token or is space after comment
if ($valueIndex === $colonIndex - 1 || $tokens[$valueIndex]->isComment()) {
continue;
}
$tokens->clearAt($valueIndex + 1);
}
}
}
}

View File

@@ -0,0 +1,240 @@
<?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\ControlStructure;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
final class SwitchContinueToBreakFixer extends AbstractFixer
{
/**
* @var list<int>
*/
private array $switchLevels = [];
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Switch case must not be ended with `continue` but with `break`.',
[
new CodeSample(
'<?php
switch ($foo) {
case 1:
continue;
}
'
),
new CodeSample(
'<?php
switch ($foo) {
case 1:
while($bar) {
do {
continue 3;
} while(false);
if ($foo + 1 > 3) {
continue;
}
continue 2;
}
}
'
),
]
);
}
/**
* {@inheritdoc}
*
* Must run after NoAlternativeSyntaxFixer.
*/
public function getPriority(): int
{
return 0;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAllTokenKindsFound([\T_SWITCH, \T_CONTINUE]) && !$tokens->hasAlternativeSyntax();
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$count = \count($tokens);
for ($index = 1; $index < $count - 1; ++$index) {
$index = $this->doFix($tokens, $index, 0, false);
}
}
/**
* @param int $depth >= 0
*/
private function doFix(Tokens $tokens, int $index, int $depth, bool $isInSwitch): int
{
$token = $tokens[$index];
if ($token->isGivenKind([\T_FOREACH, \T_FOR, \T_WHILE])) {
// go to first `(`, go to its close ')', go to first of '{', ';', '? >'
$index = $tokens->getNextTokenOfKind($index, ['(']);
$index = $tokens->getNextTokenOfKind($index, [')']);
$index = $tokens->getNextTokenOfKind($index, ['{', ';', [\T_CLOSE_TAG]]);
if (!$tokens[$index]->equals('{')) {
return $index;
}
return $this->fixInLoop($tokens, $index, $depth + 1);
}
if ($token->isGivenKind(\T_DO)) {
return $this->fixInLoop($tokens, $tokens->getNextTokenOfKind($index, ['{']), $depth + 1);
}
if ($token->isGivenKind(\T_SWITCH)) {
return $this->fixInSwitch($tokens, $index, $depth + 1);
}
if ($token->isGivenKind(\T_CONTINUE)) {
return $this->fixContinueWhenActsAsBreak($tokens, $index, $isInSwitch, $depth);
}
return $index;
}
private function fixInSwitch(Tokens $tokens, int $switchIndex, int $depth): int
{
$this->switchLevels[] = $depth;
// figure out where the switch starts
$openIndex = $tokens->getNextTokenOfKind($switchIndex, ['{']);
// figure out where the switch ends
$closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $openIndex);
for ($index = $openIndex + 1; $index < $closeIndex; ++$index) {
$index = $this->doFix($tokens, $index, $depth, true);
}
array_pop($this->switchLevels);
return $closeIndex;
}
private function fixInLoop(Tokens $tokens, int $openIndex, int $depth): int
{
$openCount = 1;
while (true) {
++$openIndex;
$token = $tokens[$openIndex];
if ($token->equals('{')) {
++$openCount;
continue;
}
if ($token->equals('}')) {
--$openCount;
if (0 === $openCount) {
break;
}
continue;
}
$openIndex = $this->doFix($tokens, $openIndex, $depth, false);
}
return $openIndex;
}
private function fixContinueWhenActsAsBreak(Tokens $tokens, int $continueIndex, bool $isInSwitch, int $depth): int
{
$followingContinueIndex = $tokens->getNextMeaningfulToken($continueIndex);
$followingContinueToken = $tokens[$followingContinueIndex];
if ($isInSwitch && $followingContinueToken->equals(';')) {
$this->replaceContinueWithBreakToken($tokens, $continueIndex); // short continue 1 notation
return $followingContinueIndex;
}
if (!$followingContinueToken->isGivenKind(\T_LNUMBER)) {
return $followingContinueIndex;
}
$afterFollowingContinueIndex = $tokens->getNextMeaningfulToken($followingContinueIndex);
if (!$tokens[$afterFollowingContinueIndex]->equals(';')) {
return $afterFollowingContinueIndex; // if next not is `;` return without fixing, for example `continue 1 ? ><?php + $a;`
}
// check if continue {jump} targets a switch statement and if so fix it
$jump = $followingContinueToken->getContent();
$jump = str_replace('_', '', $jump); // support for numeric_literal_separator
if (\strlen($jump) > 2 && 'x' === $jump[1]) {
$jump = hexdec($jump); // hexadecimal - 0x1
} elseif (\strlen($jump) > 2 && 'b' === $jump[1]) {
$jump = bindec($jump); // binary - 0b1
} elseif (\strlen($jump) > 1 && '0' === $jump[0]) {
$jump = octdec($jump); // octal 01
} elseif (Preg::match('#^\d+$#', $jump)) { // positive int
$jump = (float) $jump; // cast to float, might be a number bigger than PHP max. int value
} else {
return $afterFollowingContinueIndex; // cannot process value, ignore
}
if ($jump > \PHP_INT_MAX) {
return $afterFollowingContinueIndex; // cannot process value, ignore
}
$jump = (int) $jump;
if ($isInSwitch && (1 === $jump || 0 === $jump)) {
$this->replaceContinueWithBreakToken($tokens, $continueIndex); // long continue 0/1 notation
return $afterFollowingContinueIndex;
}
$jumpDestination = $depth - $jump + 1;
if (\in_array($jumpDestination, $this->switchLevels, true)) {
$this->replaceContinueWithBreakToken($tokens, $continueIndex);
return $afterFollowingContinueIndex;
}
return $afterFollowingContinueIndex;
}
private function replaceContinueWithBreakToken(Tokens $tokens, int $index): void
{
$tokens[$index] = new Token([\T_BREAK, 'break']);
}
}

View File

@@ -0,0 +1,246 @@
<?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\ControlStructure;
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\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
/**
* @phpstan-type _AutogeneratedInputConfiguration array{
* after_heredoc?: bool,
* elements?: list<'arguments'|'array_destructuring'|'arrays'|'match'|'parameters'>,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* after_heredoc: bool,
* elements: list<'arguments'|'array_destructuring'|'arrays'|'match'|'parameters'>,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Sebastiaan Stok <s.stok@rollerscapes.net>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
* @author Kuba Werłos <werlos@gmail.com>
*/
final class TrailingCommaInMultilineFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
/**
* @internal
*/
public const ELEMENTS_ARRAYS = 'arrays';
/**
* @internal
*/
public const ELEMENTS_ARGUMENTS = 'arguments';
/**
* @internal
*/
public const ELEMENTS_PARAMETERS = 'parameters';
private const MATCH_EXPRESSIONS = 'match';
private const ARRAY_DESTRUCTURING = 'array_destructuring';
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Arguments lists, array destructuring lists, arrays that are multi-line, `match`-lines and parameters lists must have a trailing comma.',
[
new CodeSample("<?php\narray(\n 1,\n 2\n);\n"),
new CodeSample(
<<<'SAMPLE'
<?php
$x = [
'foo',
<<<EOD
bar
EOD
];
SAMPLE,
['after_heredoc' => true]
),
new CodeSample("<?php\nfoo(\n 1,\n 2\n);\n", ['elements' => [self::ELEMENTS_ARGUMENTS]]),
new VersionSpecificCodeSample("<?php\nfunction foo(\n \$x,\n \$y\n)\n{\n}\n", new VersionSpecification(8_00_00), ['elements' => [self::ELEMENTS_PARAMETERS]]),
]
);
}
/**
* {@inheritdoc}
*
* Must run after MultilinePromotedPropertiesFixer.
*/
public function getPriority(): int
{
return 0;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound([\T_ARRAY, CT::T_ARRAY_SQUARE_BRACE_OPEN, '(', CT::T_DESTRUCTURING_SQUARE_BRACE_OPEN]);
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('after_heredoc', 'Whether a trailing comma should also be placed after heredoc end.'))
->setAllowedTypes(['bool'])
->setDefault(false) // @TODO 4.0: set to true
->getOption(),
(new FixerOptionBuilder('elements', \sprintf('Where to fix multiline trailing comma (PHP >= 8.0 for `%s` and `%s`).', self::ELEMENTS_PARAMETERS, self::MATCH_EXPRESSIONS))) // @TODO: remove text when PHP 8.0+ is required
->setAllowedTypes(['string[]'])
->setAllowedValues([
new AllowedValueSubset([
self::ARRAY_DESTRUCTURING,
self::ELEMENTS_ARGUMENTS,
self::ELEMENTS_ARRAYS,
self::ELEMENTS_PARAMETERS,
self::MATCH_EXPRESSIONS,
]),
])
->setDefault([self::ELEMENTS_ARRAYS])
->getOption(),
]);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$configuredElements = $this->configuration['elements'];
$fixArrays = \in_array(self::ELEMENTS_ARRAYS, $configuredElements, true);
$fixArguments = \in_array(self::ELEMENTS_ARGUMENTS, $configuredElements, true);
$fixParameters = \PHP_VERSION_ID >= 8_00_00 && \in_array(self::ELEMENTS_PARAMETERS, $configuredElements, true); // @TODO: drop condition when PHP 8.0+ is required
$fixMatch = \PHP_VERSION_ID >= 8_00_00 && \in_array(self::MATCH_EXPRESSIONS, $configuredElements, true); // @TODO: drop condition when PHP 8.0+ is required
$fixDestructuring = \in_array(self::ARRAY_DESTRUCTURING, $configuredElements, true);
for ($index = $tokens->count() - 1; $index >= 0; --$index) {
if ($tokens[$index]->isGivenKind(CT::T_DESTRUCTURING_SQUARE_BRACE_OPEN)) {
if ($fixDestructuring) { // array destructing short syntax
$this->fixBlock($tokens, $index);
}
continue;
}
if ($tokens[$index]->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN)) {
if ($fixArrays) { // array short syntax
$this->fixBlock($tokens, $index);
}
continue;
}
if (!$tokens[$index]->equals('(')) {
continue;
}
$prevIndex = $tokens->getPrevMeaningfulToken($index);
if ($tokens[$prevIndex]->isGivenKind(\T_ARRAY)) {
if ($fixArrays) { // array long syntax
$this->fixBlock($tokens, $index);
}
continue;
}
if ($tokens[$prevIndex]->isGivenKind(\T_LIST)) {
if ($fixDestructuring || $fixArguments) { // array destructing long syntax
$this->fixBlock($tokens, $index);
}
continue;
}
if ($fixMatch && $tokens[$prevIndex]->isGivenKind(\T_MATCH)) {
$this->fixBlock($tokens, $tokens->getNextTokenOfKind($index, ['{']));
continue;
}
$prevPrevIndex = $tokens->getPrevMeaningfulToken($prevIndex);
if ($fixArguments
&& $tokens[$prevIndex]->equalsAny([']', [\T_CLASS], [\T_STRING], [\T_VARIABLE], [\T_STATIC], [\T_ISSET], [\T_UNSET], [\T_LIST]])
&& !$tokens[$prevPrevIndex]->isGivenKind(\T_FUNCTION)
) {
$this->fixBlock($tokens, $index);
continue;
}
if (
$fixParameters
&& (
$tokens[$prevIndex]->isGivenKind(\T_STRING)
&& $tokens[$prevPrevIndex]->isGivenKind(\T_FUNCTION)
|| $tokens[$prevIndex]->isGivenKind([\T_FN, \T_FUNCTION])
)
) {
$this->fixBlock($tokens, $index);
}
}
}
private function fixBlock(Tokens $tokens, int $startIndex): void
{
$tokensAnalyzer = new TokensAnalyzer($tokens);
if (!$tokensAnalyzer->isBlockMultiline($tokens, $startIndex)) {
return;
}
$blockType = Tokens::detectBlockType($tokens[$startIndex]);
$endIndex = $tokens->findBlockEnd($blockType['type'], $startIndex);
$beforeEndIndex = $tokens->getPrevMeaningfulToken($endIndex);
if (!$tokens->isPartialCodeMultiline($beforeEndIndex, $endIndex)) {
return;
}
$beforeEndToken = $tokens[$beforeEndIndex];
// if there is some item between braces then add `,` after it
if (
$startIndex !== $beforeEndIndex && !$beforeEndToken->equals(',')
&& (true === $this->configuration['after_heredoc'] || !$beforeEndToken->isGivenKind(\T_END_HEREDOC))
) {
$tokens->insertAt($beforeEndIndex + 1, new Token(','));
$endToken = $tokens[$endIndex];
if (!$endToken->isComment() && !$endToken->isWhitespace()) {
$tokens->ensureWhitespaceAtIndex($endIndex, 1, ' ');
}
}
}
}

View File

@@ -0,0 +1,746 @@
<?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\ControlStructure;
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\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
/**
* @phpstan-type _AutogeneratedInputConfiguration array{
* always_move_variable?: bool,
* equal?: bool|null,
* identical?: bool|null,
* less_and_greater?: bool|null,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* always_move_variable: bool,
* equal: bool|null,
* identical: bool|null,
* less_and_greater: bool|null,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Bram Gotink <bram@gotink.me>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class YodaStyleFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
/**
* @var array<int|string, Token>
*/
private array $candidatesMap;
/**
* @var array<int|string, null|bool>
*/
private array $candidateTypesConfiguration;
/**
* @var list<int|string>
*/
private array $candidateTypes;
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Write conditions in Yoda style (`true`), non-Yoda style (`[\'equal\' => false, \'identical\' => false, \'less_and_greater\' => false]`) or ignore those conditions (`null`) based on configuration.',
[
new CodeSample(
'<?php
if ($a === null) {
echo "null";
}
'
),
new CodeSample(
'<?php
$b = $c != 1; // equal
$a = 1 === $b; // identical
$c = $c > 3; // less than
',
[
'equal' => true,
'identical' => false,
'less_and_greater' => null,
]
),
new CodeSample(
'<?php
return $foo === count($bar);
',
[
'always_move_variable' => true,
]
),
new CodeSample(
'<?php
// Enforce non-Yoda style.
if (null === $a) {
echo "null";
}
',
[
'equal' => false,
'identical' => false,
'less_and_greater' => false,
]
),
]
);
}
/**
* {@inheritdoc}
*
* Must run after IsNullFixer.
*/
public function getPriority(): int
{
return 0;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound($this->candidateTypes);
}
protected function configurePostNormalisation(): void
{
$this->resolveConfiguration();
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$this->fixTokens($tokens);
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('equal', 'Style for equal (`==`, `!=`) statements.'))
->setAllowedTypes(['bool', 'null'])
->setDefault(true)
->getOption(),
(new FixerOptionBuilder('identical', 'Style for identical (`===`, `!==`) statements.'))
->setAllowedTypes(['bool', 'null'])
->setDefault(true)
->getOption(),
(new FixerOptionBuilder('less_and_greater', 'Style for less and greater than (`<`, `<=`, `>`, `>=`) statements.'))
->setAllowedTypes(['bool', 'null'])
->setDefault(null)
->getOption(),
(new FixerOptionBuilder('always_move_variable', 'Whether variables should always be on non assignable side when applying Yoda style.'))
->setAllowedTypes(['bool'])
->setDefault(false)
->getOption(),
]);
}
/**
* Finds the end of the right-hand side of the comparison at the given
* index.
*
* The right-hand side ends when an operator with a lower precedence is
* encountered or when the block level for `()`, `{}` or `[]` goes below
* zero.
*
* @param Tokens $tokens The token list
* @param int $index The index of the comparison
*
* @return int The last index of the right-hand side of the comparison
*/
private function findComparisonEnd(Tokens $tokens, int $index): int
{
++$index;
$count = \count($tokens);
while ($index < $count) {
$token = $tokens[$index];
if ($token->isGivenKind([\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT])) {
++$index;
continue;
}
if ($this->isOfLowerPrecedence($token)) {
break;
}
$block = Tokens::detectBlockType($token);
if (null === $block) {
++$index;
continue;
}
if (!$block['isStart']) {
break;
}
$index = $tokens->findBlockEnd($block['type'], $index) + 1;
}
$prev = $tokens->getPrevMeaningfulToken($index);
return $tokens[$prev]->isGivenKind(\T_CLOSE_TAG) ? $tokens->getPrevMeaningfulToken($prev) : $prev;
}
/**
* Finds the start of the left-hand side of the comparison at the given
* index.
*
* The left-hand side ends when an operator with a lower precedence is
* encountered or when the block level for `()`, `{}` or `[]` goes below
* zero.
*
* @param Tokens $tokens The token list
* @param int $index The index of the comparison
*
* @return int The first index of the left-hand side of the comparison
*/
private function findComparisonStart(Tokens $tokens, int $index): int
{
--$index;
$nonBlockFound = false;
while (0 <= $index) {
$token = $tokens[$index];
if ($token->isGivenKind([\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT])) {
--$index;
continue;
}
if ($token->isGivenKind(CT::T_NAMED_ARGUMENT_COLON)) {
break;
}
if ($this->isOfLowerPrecedence($token)) {
break;
}
$block = Tokens::detectBlockType($token);
if (null === $block) {
--$index;
$nonBlockFound = true;
continue;
}
if (
$block['isStart']
|| ($nonBlockFound && Tokens::BLOCK_TYPE_CURLY_BRACE === $block['type']) // closing of structure not related to the comparison
) {
break;
}
$index = $tokens->findBlockStart($block['type'], $index) - 1;
}
return $tokens->getNextMeaningfulToken($index);
}
private function fixTokens(Tokens $tokens): Tokens
{
for ($i = \count($tokens) - 1; $i > 1; --$i) {
if ($tokens[$i]->isGivenKind($this->candidateTypes)) {
$yoda = $this->candidateTypesConfiguration[$tokens[$i]->getId()];
} elseif (
($tokens[$i]->equals('<') && \in_array('<', $this->candidateTypes, true))
|| ($tokens[$i]->equals('>') && \in_array('>', $this->candidateTypes, true))
) {
$yoda = $this->candidateTypesConfiguration[$tokens[$i]->getContent()];
} else {
continue;
}
$fixableCompareInfo = $this->getCompareFixableInfo($tokens, $i, $yoda);
if (null === $fixableCompareInfo) {
continue;
}
$i = $this->fixTokensCompare(
$tokens,
$fixableCompareInfo['left']['start'],
$fixableCompareInfo['left']['end'],
$i,
$fixableCompareInfo['right']['start'],
$fixableCompareInfo['right']['end']
);
}
return $tokens;
}
/**
* Fixes the comparison at the given index.
*
* A comparison is considered fixed when
* - both sides are a variable (e.g. $a === $b)
* - neither side is a variable (e.g. self::CONST === 3)
* - only the right-hand side is a variable (e.g. 3 === self::$var)
*
* If the left-hand side and right-hand side of the given comparison are
* swapped, this function runs recursively on the previous left-hand-side.
*
* @return int an upper bound for all non-fixed comparisons
*/
private function fixTokensCompare(
Tokens $tokens,
int $startLeft,
int $endLeft,
int $compareOperatorIndex,
int $startRight,
int $endRight
): int {
$type = $tokens[$compareOperatorIndex]->getId();
$content = $tokens[$compareOperatorIndex]->getContent();
if (\array_key_exists($type, $this->candidatesMap)) {
$tokens[$compareOperatorIndex] = clone $this->candidatesMap[$type];
} elseif (\array_key_exists($content, $this->candidatesMap)) {
$tokens[$compareOperatorIndex] = clone $this->candidatesMap[$content];
}
$right = $this->fixTokensComparePart($tokens, $startRight, $endRight);
$left = $this->fixTokensComparePart($tokens, $startLeft, $endLeft);
for ($i = $startRight; $i <= $endRight; ++$i) {
$tokens->clearAt($i);
}
for ($i = $startLeft; $i <= $endLeft; ++$i) {
$tokens->clearAt($i);
}
$tokens->insertAt($startRight, $left);
$tokens->insertAt($startLeft, $right);
return $startLeft;
}
private function fixTokensComparePart(Tokens $tokens, int $start, int $end): Tokens
{
$newTokens = $tokens->generatePartialCode($start, $end);
$newTokens = $this->fixTokens(Tokens::fromCode(\sprintf('<?php %s;', $newTokens)));
$newTokens->clearAt(\count($newTokens) - 1);
$newTokens->clearAt(0);
$newTokens->clearEmptyTokens();
return $newTokens;
}
/**
* @return null|array{left: array{start: int, end: int}, right: array{start: int, end: int}}
*/
private function getCompareFixableInfo(Tokens $tokens, int $index, bool $yoda): ?array
{
$right = $this->getRightSideCompareFixableInfo($tokens, $index);
if (!$yoda && $this->isOfLowerPrecedenceAssignment($tokens[$tokens->getNextMeaningfulToken($right['end'])])) {
return null;
}
$left = $this->getLeftSideCompareFixableInfo($tokens, $index);
if ($this->isListStatement($tokens, $left['start'], $left['end']) || $this->isListStatement($tokens, $right['start'], $right['end'])) {
return null; // do not fix lists assignment inside statements
}
/** @var bool $strict */
$strict = $this->configuration['always_move_variable'];
$leftSideIsVariable = $this->isVariable($tokens, $left['start'], $left['end'], $strict);
$rightSideIsVariable = $this->isVariable($tokens, $right['start'], $right['end'], $strict);
if (!($leftSideIsVariable xor $rightSideIsVariable)) {
return null; // both are (not) variables, do not touch
}
if (!$strict) { // special handling for braces with not "always_move_variable"
$leftSideIsVariable = $leftSideIsVariable && !$tokens[$left['start']]->equals('(');
$rightSideIsVariable = $rightSideIsVariable && !$tokens[$right['start']]->equals('(');
}
return ($yoda && !$leftSideIsVariable) || (!$yoda && !$rightSideIsVariable)
? null
: ['left' => $left, 'right' => $right];
}
/**
* @return array{start: int, end: int}
*/
private function getLeftSideCompareFixableInfo(Tokens $tokens, int $index): array
{
return [
'start' => $this->findComparisonStart($tokens, $index),
'end' => $tokens->getPrevMeaningfulToken($index),
];
}
/**
* @return array{start: int, end: int}
*/
private function getRightSideCompareFixableInfo(Tokens $tokens, int $index): array
{
return [
'start' => $tokens->getNextMeaningfulToken($index),
'end' => $this->findComparisonEnd($tokens, $index),
];
}
private function isListStatement(Tokens $tokens, int $index, int $end): bool
{
for ($i = $index; $i <= $end; ++$i) {
if ($tokens[$i]->isGivenKind([\T_LIST, CT::T_DESTRUCTURING_SQUARE_BRACE_OPEN, CT::T_DESTRUCTURING_SQUARE_BRACE_CLOSE])) {
return true;
}
}
return false;
}
/**
* Checks whether the given token has a lower precedence than `T_IS_EQUAL`
* or `T_IS_IDENTICAL`.
*
* @param Token $token The token to check
*
* @return bool Whether the token has a lower precedence
*/
private function isOfLowerPrecedence(Token $token): bool
{
return $this->isOfLowerPrecedenceAssignment($token)
|| $token->isGivenKind([
\T_BOOLEAN_AND, // &&
\T_BOOLEAN_OR, // ||
\T_CASE, // case
\T_DOUBLE_ARROW, // =>
\T_ECHO, // echo
\T_GOTO, // goto
\T_LOGICAL_AND, // and
\T_LOGICAL_OR, // or
\T_LOGICAL_XOR, // xor
\T_OPEN_TAG, // <?php
\T_OPEN_TAG_WITH_ECHO,
\T_PRINT, // print
\T_RETURN, // return
\T_THROW, // throw
\T_COALESCE,
\T_YIELD, // yield
\T_YIELD_FROM,
\T_REQUIRE,
\T_REQUIRE_ONCE,
\T_INCLUDE,
\T_INCLUDE_ONCE,
])
|| $token->equalsAny([
// bitwise and, or, xor
'&', '|', '^',
// ternary operators
'?', ':',
// end of PHP statement
',', ';',
]);
}
/**
* Checks whether the given assignment token has a lower precedence than `T_IS_EQUAL`
* or `T_IS_IDENTICAL`.
*/
private function isOfLowerPrecedenceAssignment(Token $token): bool
{
return $token->equals('=') || $token->isGivenKind([
\T_AND_EQUAL, // &=
\T_CONCAT_EQUAL, // .=
\T_DIV_EQUAL, // /=
\T_MINUS_EQUAL, // -=
\T_MOD_EQUAL, // %=
\T_MUL_EQUAL, // *=
\T_OR_EQUAL, // |=
\T_PLUS_EQUAL, // +=
\T_POW_EQUAL, // **=
\T_SL_EQUAL, // <<=
\T_SR_EQUAL, // >>=
\T_XOR_EQUAL, // ^=
\T_COALESCE_EQUAL, // ??=
]);
}
/**
* Checks whether the tokens between the given start and end describe a
* variable.
*
* @param Tokens $tokens The token list
* @param int $start The first index of the possible variable
* @param int $end The last index of the possible variable
* @param bool $strict Enable strict variable detection
*
* @return bool Whether the tokens describe a variable
*/
private function isVariable(Tokens $tokens, int $start, int $end, bool $strict): bool
{
$tokenAnalyzer = new TokensAnalyzer($tokens);
if ($start === $end) {
return $tokens[$start]->isGivenKind(\T_VARIABLE);
}
if ($tokens[$start]->equals('(')) {
return true;
}
if ($strict) {
for ($index = $start; $index <= $end; ++$index) {
if (
$tokens[$index]->isCast()
|| $tokens[$index]->isGivenKind(\T_INSTANCEOF)
|| $tokens[$index]->equals('!')
|| $tokenAnalyzer->isBinaryOperator($index)
) {
return false;
}
}
}
$index = $start;
// handle multiple braces around statement ((($a === 1)))
while (
$tokens[$index]->equals('(')
&& $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index) === $end
) {
$index = $tokens->getNextMeaningfulToken($index);
$end = $tokens->getPrevMeaningfulToken($end);
}
$expectString = false;
while ($index <= $end) {
$current = $tokens[$index];
if ($current->isComment() || $current->isWhitespace() || $tokens->isEmptyAt($index)) {
++$index;
continue;
}
// check if this is the last token
if ($index === $end) {
return $current->isGivenKind($expectString ? \T_STRING : \T_VARIABLE);
}
if ($current->isGivenKind([\T_LIST, CT::T_DESTRUCTURING_SQUARE_BRACE_OPEN, CT::T_DESTRUCTURING_SQUARE_BRACE_CLOSE])) {
return false;
}
$nextIndex = $tokens->getNextMeaningfulToken($index);
$next = $tokens[$nextIndex];
// self:: or ClassName::
if ($current->isGivenKind(\T_STRING) && $next->isGivenKind(\T_DOUBLE_COLON)) {
$index = $tokens->getNextMeaningfulToken($nextIndex);
continue;
}
// \ClassName
if ($current->isGivenKind(\T_NS_SEPARATOR) && $next->isGivenKind(\T_STRING)) {
$index = $nextIndex;
continue;
}
// ClassName\
if ($current->isGivenKind(\T_STRING) && $next->isGivenKind(\T_NS_SEPARATOR)) {
$index = $nextIndex;
continue;
}
// $a-> or a-> (as in $b->a->c)
if ($current->isGivenKind([\T_STRING, \T_VARIABLE]) && $next->isObjectOperator()) {
$index = $tokens->getNextMeaningfulToken($nextIndex);
$expectString = true;
continue;
}
// $a[...], a[...] (as in $c->a[$b]), $a{...} or a{...} (as in $c->a{$b})
if (
$current->isGivenKind($expectString ? \T_STRING : \T_VARIABLE)
&& $next->equalsAny(['[', [CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN, '{']])
) {
$index = $tokens->findBlockEnd(
$next->equals('[') ? Tokens::BLOCK_TYPE_INDEX_SQUARE_BRACE : Tokens::BLOCK_TYPE_ARRAY_INDEX_CURLY_BRACE,
$nextIndex
);
if ($index === $end) {
return true;
}
$index = $tokens->getNextMeaningfulToken($index);
if (!$tokens[$index]->equalsAny(['[', [CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN, '{']]) && !$tokens[$index]->isObjectOperator()) {
return false;
}
$index = $tokens->getNextMeaningfulToken($index);
$expectString = true;
continue;
}
// $a(...) or $a->b(...)
if ($strict && $current->isGivenKind([\T_STRING, \T_VARIABLE]) && $next->equals('(')) {
return false;
}
// {...} (as in $a->{$b})
if ($expectString && $current->isGivenKind(CT::T_DYNAMIC_PROP_BRACE_OPEN)) {
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_DYNAMIC_PROP_BRACE, $index);
if ($index === $end) {
return true;
}
$index = $tokens->getNextMeaningfulToken($index);
if (!$tokens[$index]->isObjectOperator()) {
return false;
}
$index = $tokens->getNextMeaningfulToken($index);
$expectString = true;
continue;
}
break;
}
return !$this->isConstant($tokens, $start, $end);
}
private function isConstant(Tokens $tokens, int $index, int $end): bool
{
$expectArrayOnly = false;
$expectNumberOnly = false;
$expectNothing = false;
for (; $index <= $end; ++$index) {
$token = $tokens[$index];
if ($token->isComment() || $token->isWhitespace()) {
continue;
}
if ($expectNothing) {
return false;
}
if ($expectArrayOnly) {
if ($token->equalsAny(['(', ')', [CT::T_ARRAY_SQUARE_BRACE_CLOSE]])) {
continue;
}
return false;
}
if ($token->isGivenKind([\T_ARRAY, CT::T_ARRAY_SQUARE_BRACE_OPEN])) {
$expectArrayOnly = true;
continue;
}
if ($expectNumberOnly && !$token->isGivenKind([\T_LNUMBER, \T_DNUMBER])) {
return false;
}
if ($token->equals('-')) {
$expectNumberOnly = true;
continue;
}
if (
$token->isGivenKind([\T_LNUMBER, \T_DNUMBER, \T_CONSTANT_ENCAPSED_STRING])
|| $token->equalsAny([[\T_STRING, 'true'], [\T_STRING, 'false'], [\T_STRING, 'null']])
) {
$expectNothing = true;
continue;
}
return false;
}
return true;
}
private function resolveConfiguration(): void
{
$candidateTypes = [];
$this->candidatesMap = [];
if (null !== $this->configuration['equal']) {
// `==`, `!=` and `<>`
$candidateTypes[\T_IS_EQUAL] = $this->configuration['equal'];
$candidateTypes[\T_IS_NOT_EQUAL] = $this->configuration['equal'];
}
if (null !== $this->configuration['identical']) {
// `===` and `!==`
$candidateTypes[\T_IS_IDENTICAL] = $this->configuration['identical'];
$candidateTypes[\T_IS_NOT_IDENTICAL] = $this->configuration['identical'];
}
if (null !== $this->configuration['less_and_greater']) {
// `<`, `<=`, `>` and `>=`
$candidateTypes[\T_IS_SMALLER_OR_EQUAL] = $this->configuration['less_and_greater'];
$this->candidatesMap[\T_IS_SMALLER_OR_EQUAL] = new Token([\T_IS_GREATER_OR_EQUAL, '>=']);
$candidateTypes[\T_IS_GREATER_OR_EQUAL] = $this->configuration['less_and_greater'];
$this->candidatesMap[\T_IS_GREATER_OR_EQUAL] = new Token([\T_IS_SMALLER_OR_EQUAL, '<=']);
$candidateTypes['<'] = $this->configuration['less_and_greater'];
$this->candidatesMap['<'] = new Token('>');
$candidateTypes['>'] = $this->configuration['less_and_greater'];
$this->candidatesMap['>'] = new Token('<');
}
$this->candidateTypesConfiguration = $candidateTypes;
$this->candidateTypes = array_keys($candidateTypes);
}
}