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,71 @@
<?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\Operator;
use PhpCsFixer\Fixer\AbstractShortOperatorFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
final class AssignNullCoalescingToCoalesceEqualFixer extends AbstractShortOperatorFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Use the null coalescing assignment operator `??=` where possible.',
[
new CodeSample(
"<?php\n\$foo = \$foo ?? 1;\n",
),
]
);
}
/**
* {@inheritdoc}
*
* Must run before BinaryOperatorSpacesFixer, NoWhitespaceInBlankLineFixer.
* Must run after TernaryToNullCoalescingFixer.
*/
public function getPriority(): int
{
return -1;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(\T_COALESCE);
}
protected function isOperatorTokenCandidate(Tokens $tokens, int $index): bool
{
if (!$tokens[$index]->isGivenKind(\T_COALESCE)) {
return false;
}
// make sure after '??' does not contain '? :'
$nextIndex = $tokens->getNextTokenOfKind($index, ['?', ';', [\T_CLOSE_TAG]]);
return !$tokens[$nextIndex]->equals('?');
}
protected function getReplacementToken(Token $token): Token
{
return new Token([\T_COALESCE_EQUAL, '??=']);
}
}

View File

@@ -0,0 +1,960 @@
<?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\Operator;
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\Preg;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
use PhpCsFixer\Utils;
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
/**
* @phpstan-type _AutogeneratedInputConfiguration array{
* default?: 'align'|'align_by_scope'|'align_single_space'|'align_single_space_by_scope'|'align_single_space_minimal'|'align_single_space_minimal_by_scope'|'at_least_single_space'|'no_space'|'single_space'|null,
* operators?: array<string, ?string>,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* default: 'align'|'align_by_scope'|'align_single_space'|'align_single_space_by_scope'|'align_single_space_minimal'|'align_single_space_minimal_by_scope'|'at_least_single_space'|'no_space'|'single_space'|null,
* operators: array<string, ?string>,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class BinaryOperatorSpacesFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
/**
* @internal
*/
public const SINGLE_SPACE = 'single_space';
/**
* @internal
*/
public const AT_LEAST_SINGLE_SPACE = 'at_least_single_space';
/**
* @internal
*/
public const NO_SPACE = 'no_space';
/**
* @internal
*/
public const ALIGN = 'align';
/**
* @internal
*/
public const ALIGN_BY_SCOPE = 'align_by_scope';
/**
* @internal
*/
public const ALIGN_SINGLE_SPACE = 'align_single_space';
/**
* @internal
*/
public const ALIGN_SINGLE_SPACE_BY_SCOPE = 'align_single_space_by_scope';
/**
* @internal
*/
public const ALIGN_SINGLE_SPACE_MINIMAL = 'align_single_space_minimal';
/**
* @internal
*/
public const ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE = 'align_single_space_minimal_by_scope';
/**
* @internal
*
* @const Placeholder used as anchor for right alignment.
*/
public const ALIGN_PLACEHOLDER = "\x2 ALIGNABLE%d \x3";
/**
* @var list<string>
*/
private const SUPPORTED_OPERATORS = [
'=',
'*',
'/',
'%',
'<',
'>',
'|',
'^',
'+',
'-',
'&',
'&=',
'&&',
'||',
'.=',
'/=',
'=>',
'==',
'>=',
'===',
'!=',
'<>',
'!==',
'<=',
'and',
'or',
'xor',
'-=',
'%=',
'*=',
'|=',
'+=',
'<<',
'<<=',
'>>',
'>>=',
'^=',
'**',
'**=',
'<=>',
'??',
'??=',
];
/**
* @var list<null|string>
*/
private const ALLOWED_VALUES = [
self::ALIGN,
self::ALIGN_BY_SCOPE,
self::ALIGN_SINGLE_SPACE,
self::ALIGN_SINGLE_SPACE_MINIMAL,
self::ALIGN_SINGLE_SPACE_BY_SCOPE,
self::ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE,
self::SINGLE_SPACE,
self::NO_SPACE,
self::AT_LEAST_SINGLE_SPACE,
null,
];
/**
* Keep track of the deepest level ever achieved while
* parsing the code. Used later to replace alignment
* placeholders with spaces.
*/
private int $deepestLevel;
/**
* Level counter of the current nest level.
* So one level alignments are not mixed with
* other level ones.
*/
private int $currentLevel;
private TokensAnalyzer $tokensAnalyzer;
/**
* @var array<string, string>
*/
private array $alignOperatorTokens = [];
/**
* @var array<string, string>
*/
private array $operators = [];
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Binary operators should be surrounded by space as configured.',
[
new CodeSample(
"<?php\n\$a= 1 + \$b^ \$d !== \$e or \$f;\n"
),
new CodeSample(
'<?php
$aa= 1;
$b=2;
$c = $d xor $e;
$f -= 1;
',
['operators' => ['=' => self::ALIGN, 'xor' => null]]
),
new CodeSample(
'<?php
$a = $b +=$c;
$d = $ee+=$f;
$g = $b +=$c;
$h = $ee+=$f;
',
['operators' => ['+=' => self::ALIGN_SINGLE_SPACE]]
),
new CodeSample(
'<?php
$a = $b===$c;
$d = $f === $g;
$h = $i=== $j;
',
['operators' => ['===' => self::ALIGN_SINGLE_SPACE_MINIMAL]]
),
new CodeSample(
'<?php
$foo = \json_encode($bar, JSON_PRESERVE_ZERO_FRACTION | JSON_PRETTY_PRINT);
',
['operators' => ['|' => self::NO_SPACE]]
),
new CodeSample(
'<?php
$array = [
"foo" => 1,
"baaaaaaaaaaar" => 11,
];
',
['operators' => ['=>' => self::SINGLE_SPACE]]
),
new CodeSample(
'<?php
$array = [
"foo" => 12,
"baaaaaaaaaaar" => 13,
"baz" => 1,
];
',
['operators' => ['=>' => self::ALIGN]]
),
new CodeSample(
'<?php
$array = [
"foo" => 12,
"baaaaaaaaaaar" => 13,
"baz" => 1,
];
',
['operators' => ['=>' => self::ALIGN_BY_SCOPE]]
),
new CodeSample(
'<?php
$array = [
"foo" => 12,
"baaaaaaaaaaar" => 13,
"baz" => 1,
];
',
['operators' => ['=>' => self::ALIGN_SINGLE_SPACE]]
),
new CodeSample(
'<?php
$array = [
"foo" => 12,
"baaaaaaaaaaar" => 13,
"baz" => 1,
];
',
['operators' => ['=>' => self::ALIGN_SINGLE_SPACE_BY_SCOPE]]
),
new CodeSample(
'<?php
$array = [
"foo" => 12,
"baaaaaaaaaaar" => 13,
"baz" => 1,
];
',
['operators' => ['=>' => self::ALIGN_SINGLE_SPACE_MINIMAL]]
),
new CodeSample(
'<?php
$array = [
"foo" => 12,
"baaaaaaaaaaar" => 13,
"baz" => 1,
];
',
['operators' => ['=>' => self::ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE]]
),
]
);
}
/**
* {@inheritdoc}
*
* Must run after ArrayIndentationFixer, ArraySyntaxFixer, AssignNullCoalescingToCoalesceEqualFixer, ListSyntaxFixer, LongToShorthandOperatorFixer, ModernizeStrposFixer, NoMultilineWhitespaceAroundDoubleArrowFixer, NoUnsetCastFixer, PowToExponentiationFixer, StandardizeNotEqualsFixer, StrictComparisonFixer.
*/
public function getPriority(): int
{
return -32;
}
public function isCandidate(Tokens $tokens): bool
{
return true;
}
protected function configurePostNormalisation(): void
{
$this->operators = $this->resolveOperatorsFromConfig();
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$this->tokensAnalyzer = new TokensAnalyzer($tokens);
// last and first tokens cannot be an operator
for ($index = $tokens->count() - 2; $index > 0; --$index) {
if (!$this->tokensAnalyzer->isBinaryOperator($index)) {
continue;
}
if ('=' === $tokens[$index]->getContent()) {
$isDeclare = $this->isEqualPartOfDeclareStatement($tokens, $index);
if (false === $isDeclare) {
$this->fixWhiteSpaceAroundOperator($tokens, $index);
} else {
$index = $isDeclare; // skip `declare(foo ==bar)`, see `declare_equal_normalize`
}
} else {
$this->fixWhiteSpaceAroundOperator($tokens, $index);
}
// previous of binary operator is now never an operator / previous of declare statement cannot be an operator
--$index;
}
if (\count($this->alignOperatorTokens) > 0) {
$this->fixAlignment($tokens, $this->alignOperatorTokens);
}
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('default', 'Default fix strategy.'))
->setDefault(self::SINGLE_SPACE)
->setAllowedValues(self::ALLOWED_VALUES)
->getOption(),
(new FixerOptionBuilder('operators', 'Dictionary of `binary operator` => `fix strategy` values that differ from the default strategy. Supported are: '.Utils::naturalLanguageJoinWithBackticks(self::SUPPORTED_OPERATORS).'.'))
->setAllowedTypes(['array<string, ?string>'])
->setAllowedValues([static function (array $option): bool {
foreach ($option as $operator => $value) {
if (!\in_array($operator, self::SUPPORTED_OPERATORS, true)) {
throw new InvalidOptionsException(
\sprintf(
'Unexpected "operators" key, expected any of %s, got "%s".',
Utils::naturalLanguageJoin(self::SUPPORTED_OPERATORS),
\gettype($operator).'#'.$operator
)
);
}
if (!\in_array($value, self::ALLOWED_VALUES, true)) {
throw new InvalidOptionsException(
\sprintf(
'Unexpected value for operator "%s", expected any of %s, got "%s".',
$operator,
Utils::naturalLanguageJoin(array_map(
static fn ($value): string => Utils::toString($value),
self::ALLOWED_VALUES
)),
\is_object($value) ? \get_class($value) : (null === $value ? 'null' : \gettype($value).'#'.$value)
)
);
}
}
return true;
}])
->setDefault([])
->getOption(),
]);
}
private function fixWhiteSpaceAroundOperator(Tokens $tokens, int $index): void
{
$tokenContent = strtolower($tokens[$index]->getContent());
if (!\array_key_exists($tokenContent, $this->operators)) {
return; // not configured to be changed
}
if (self::SINGLE_SPACE === $this->operators[$tokenContent]) {
$this->fixWhiteSpaceAroundOperatorToSingleSpace($tokens, $index);
return;
}
if (self::AT_LEAST_SINGLE_SPACE === $this->operators[$tokenContent]) {
$this->fixWhiteSpaceAroundOperatorToAtLeastSingleSpace($tokens, $index);
return;
}
if (self::NO_SPACE === $this->operators[$tokenContent]) {
$this->fixWhiteSpaceAroundOperatorToNoSpace($tokens, $index);
return;
}
// schedule for alignment
$this->alignOperatorTokens[$tokenContent] = $this->operators[$tokenContent];
if (
self::ALIGN === $this->operators[$tokenContent]
|| self::ALIGN_BY_SCOPE === $this->operators[$tokenContent]
) {
return;
}
// fix white space after operator
if ($tokens[$index + 1]->isWhitespace()) {
if (
self::ALIGN_SINGLE_SPACE_MINIMAL === $this->operators[$tokenContent]
|| self::ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE === $this->operators[$tokenContent]
) {
$tokens[$index + 1] = new Token([\T_WHITESPACE, ' ']);
}
return;
}
$tokens->insertAt($index + 1, new Token([\T_WHITESPACE, ' ']));
}
private function fixWhiteSpaceAroundOperatorToSingleSpace(Tokens $tokens, int $index): void
{
// fix white space after operator
if ($tokens[$index + 1]->isWhitespace()) {
$content = $tokens[$index + 1]->getContent();
if (' ' !== $content && !str_contains($content, "\n") && !$tokens[$tokens->getNextNonWhitespace($index + 1)]->isComment()) {
$tokens[$index + 1] = new Token([\T_WHITESPACE, ' ']);
}
} else {
$tokens->insertAt($index + 1, new Token([\T_WHITESPACE, ' ']));
}
// fix white space before operator
if ($tokens[$index - 1]->isWhitespace()) {
$content = $tokens[$index - 1]->getContent();
if (' ' !== $content && !str_contains($content, "\n") && !$tokens[$tokens->getPrevNonWhitespace($index - 1)]->isComment()) {
$tokens[$index - 1] = new Token([\T_WHITESPACE, ' ']);
}
} else {
$tokens->insertAt($index, new Token([\T_WHITESPACE, ' ']));
}
}
private function fixWhiteSpaceAroundOperatorToAtLeastSingleSpace(Tokens $tokens, int $index): void
{
// fix white space after operator
if (!$tokens[$index + 1]->isWhitespace()) {
$tokens->insertAt($index + 1, new Token([\T_WHITESPACE, ' ']));
}
// fix white space before operator
if (!$tokens[$index - 1]->isWhitespace()) {
$tokens->insertAt($index, new Token([\T_WHITESPACE, ' ']));
}
}
private function fixWhiteSpaceAroundOperatorToNoSpace(Tokens $tokens, int $index): void
{
// fix white space after operator
if ($tokens[$index + 1]->isWhitespace()) {
$content = $tokens[$index + 1]->getContent();
if (!str_contains($content, "\n") && !$tokens[$tokens->getNextNonWhitespace($index + 1)]->isComment()) {
$tokens->clearAt($index + 1);
}
}
// fix white space before operator
if ($tokens[$index - 1]->isWhitespace()) {
$content = $tokens[$index - 1]->getContent();
if (!str_contains($content, "\n") && !$tokens[$tokens->getPrevNonWhitespace($index - 1)]->isComment()) {
$tokens->clearAt($index - 1);
}
}
}
/**
* @return false|int index of T_DECLARE where the `=` belongs to or `false`
*/
private function isEqualPartOfDeclareStatement(Tokens $tokens, int $index)
{
$prevMeaningfulIndex = $tokens->getPrevMeaningfulToken($index);
if ($tokens[$prevMeaningfulIndex]->isGivenKind(\T_STRING)) {
$prevMeaningfulIndex = $tokens->getPrevMeaningfulToken($prevMeaningfulIndex);
if ($tokens[$prevMeaningfulIndex]->equals('(')) {
$prevMeaningfulIndex = $tokens->getPrevMeaningfulToken($prevMeaningfulIndex);
if ($tokens[$prevMeaningfulIndex]->isGivenKind(\T_DECLARE)) {
return $prevMeaningfulIndex;
}
}
}
return false;
}
/**
* @return array<string, string>
*/
private function resolveOperatorsFromConfig(): array
{
$operators = [];
if (null !== $this->configuration['default']) {
foreach (self::SUPPORTED_OPERATORS as $operator) {
$operators[$operator] = $this->configuration['default'];
}
}
foreach ($this->configuration['operators'] as $operator => $value) {
if (null === $value) {
unset($operators[$operator]);
} else {
$operators[$operator] = $value;
}
}
return $operators;
}
// Alignment logic related methods
/**
* @param array<string, string> $toAlign
*/
private function fixAlignment(Tokens $tokens, array $toAlign): void
{
$this->deepestLevel = 0;
$this->currentLevel = 0;
foreach ($toAlign as $tokenContent => $alignStrategy) {
// This fixer works partially on Tokens and partially on string representation of code.
// During the process of fixing internal state of single Token may be affected by injecting ALIGN_PLACEHOLDER to its content.
// The placeholder will be resolved by `replacePlaceholders` method by removing placeholder or changing it into spaces.
// That way of fixing the code causes disturbances in marking Token as changed - if code is perfectly valid then placeholder
// still be injected and removed, which will cause the `changed` flag to be set.
// To handle that unwanted behavior we work on clone of Tokens collection and then override original collection with fixed collection.
$tokensClone = clone $tokens;
if ('=>' === $tokenContent) {
$this->injectAlignmentPlaceholdersForArrow($tokensClone, 0, \count($tokens));
} else {
$this->injectAlignmentPlaceholdersDefault($tokensClone, 0, \count($tokens), $tokenContent);
}
// for all tokens that should be aligned but do not have anything to align with, fix spacing if needed
if (
self::ALIGN_SINGLE_SPACE === $alignStrategy
|| self::ALIGN_SINGLE_SPACE_MINIMAL === $alignStrategy
|| self::ALIGN_SINGLE_SPACE_BY_SCOPE === $alignStrategy
|| self::ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE === $alignStrategy
) {
if ('=>' === $tokenContent) {
for ($index = $tokens->count() - 2; $index > 0; --$index) {
if ($tokens[$index]->isGivenKind(\T_DOUBLE_ARROW)) { // always binary operator, never part of declare statement
$this->fixWhiteSpaceBeforeOperator($tokensClone, $index, $alignStrategy);
}
}
} elseif ('=' === $tokenContent) {
for ($index = $tokens->count() - 2; $index > 0; --$index) {
if ('=' === $tokens[$index]->getContent() && false === $this->isEqualPartOfDeclareStatement($tokens, $index) && $this->tokensAnalyzer->isBinaryOperator($index)) {
$this->fixWhiteSpaceBeforeOperator($tokensClone, $index, $alignStrategy);
}
}
} else {
for ($index = $tokens->count() - 2; $index > 0; --$index) {
$content = $tokens[$index]->getContent();
if (strtolower($content) === $tokenContent && $this->tokensAnalyzer->isBinaryOperator($index)) { // never part of declare statement
$this->fixWhiteSpaceBeforeOperator($tokensClone, $index, $alignStrategy);
}
}
}
}
$tokens->setCode($this->replacePlaceholders($tokensClone, $alignStrategy, $tokenContent));
}
}
private function injectAlignmentPlaceholdersDefault(Tokens $tokens, int $startAt, int $endAt, string $tokenContent): void
{
$newLineFoundSinceLastPlaceholder = true;
for ($index = $startAt; $index < $endAt; ++$index) {
$token = $tokens[$index];
$content = $token->getContent();
if (str_contains($content, "\n")) {
$newLineFoundSinceLastPlaceholder = true;
}
if (
strtolower($content) === $tokenContent
&& $this->tokensAnalyzer->isBinaryOperator($index)
&& ('=' !== $content || false === $this->isEqualPartOfDeclareStatement($tokens, $index))
&& $newLineFoundSinceLastPlaceholder
) {
$tokens[$index] = new Token(\sprintf(self::ALIGN_PLACEHOLDER, $this->currentLevel).$content);
$newLineFoundSinceLastPlaceholder = false;
continue;
}
if ($token->isGivenKind(\T_FN)) {
$from = $tokens->getNextMeaningfulToken($index);
$until = $this->tokensAnalyzer->getLastTokenIndexOfArrowFunction($index);
$this->injectAlignmentPlaceholders($tokens, $from + 1, $until - 1, $tokenContent);
$index = $until;
continue;
}
if ($token->isGivenKind([\T_FUNCTION, \T_CLASS])) {
$index = $tokens->getNextTokenOfKind($index, ['{', ';', '(']);
// We don't align `=` on multi-line definition of function parameters with default values
if ($tokens[$index]->equals('(')) {
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
continue;
}
if ($tokens[$index]->equals(';')) {
continue;
}
// Update the token to the `{` one in order to apply the following logic
$token = $tokens[$index];
}
if ($token->equals('{')) {
$until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
$this->injectAlignmentPlaceholders($tokens, $index + 1, $until - 1, $tokenContent);
$index = $until;
continue;
}
if ($token->equals('(')) {
$until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
$this->injectAlignmentPlaceholders($tokens, $index + 1, $until - 1, $tokenContent);
$index = $until;
continue;
}
if ($token->equals('[')) {
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_INDEX_SQUARE_BRACE, $index);
continue;
}
if ($token->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN)) {
$until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $index);
$this->injectAlignmentPlaceholders($tokens, $index + 1, $until - 1, $tokenContent);
$index = $until;
continue;
}
}
}
private function injectAlignmentPlaceholders(Tokens $tokens, int $from, int $until, string $tokenContent): void
{
// Only inject placeholders for multi-line code
if ($tokens->isPartialCodeMultiline($from, $until)) {
++$this->deepestLevel;
$currentLevel = $this->currentLevel;
$this->currentLevel = $this->deepestLevel;
$this->injectAlignmentPlaceholdersDefault($tokens, $from, $until, $tokenContent);
$this->currentLevel = $currentLevel;
}
}
private function injectAlignmentPlaceholdersForArrow(Tokens $tokens, int $startAt, int $endAt): void
{
$newLineFoundSinceLastPlaceholder = true;
$yieldFoundSinceLastPlaceholder = false;
for ($index = $startAt; $index < $endAt; ++$index) {
$token = $tokens[$index];
$content = $token->getContent();
if (str_contains($content, "\n")) {
$newLineFoundSinceLastPlaceholder = true;
}
if ($token->isGivenKind(\T_YIELD)) {
$yieldFoundSinceLastPlaceholder = true;
}
if ($token->isGivenKind(\T_FN)) {
$yieldFoundSinceLastPlaceholder = false;
$from = $tokens->getNextMeaningfulToken($index);
$until = $this->tokensAnalyzer->getLastTokenIndexOfArrowFunction($index);
$this->injectArrayAlignmentPlaceholders($tokens, $from + 1, $until - 1);
$index = $until;
continue;
}
if ($token->isGivenKind(\T_ARRAY)) { // don't use "$tokens->isArray()" here, short arrays are handled in the next case
$yieldFoundSinceLastPlaceholder = false;
$from = $tokens->getNextMeaningfulToken($index);
$until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $from);
$index = $until;
$this->injectArrayAlignmentPlaceholders($tokens, $from + 1, $until - 1);
continue;
}
if ($token->isGivenKind(CT::T_ARRAY_SQUARE_BRACE_OPEN)) {
$yieldFoundSinceLastPlaceholder = false;
$from = $index;
$until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $from);
$index = $until;
$this->injectArrayAlignmentPlaceholders($tokens, $from + 1, $until - 1);
continue;
}
// no need to analyze for `isBinaryOperator` (always true), nor if part of declare statement (not valid PHP)
// there is also no need to analyse the second arrow of a line
if ($token->isGivenKind(\T_DOUBLE_ARROW) && $newLineFoundSinceLastPlaceholder) {
if ($yieldFoundSinceLastPlaceholder) {
++$this->deepestLevel;
++$this->currentLevel;
}
$tokenContent = \sprintf(self::ALIGN_PLACEHOLDER, $this->currentLevel).$token->getContent();
$nextToken = $tokens[$index + 1];
if (!$nextToken->isWhitespace()) {
$tokenContent .= ' ';
} elseif ($nextToken->isWhitespace(" \t")) {
$tokens[$index + 1] = new Token([\T_WHITESPACE, ' ']);
}
$tokens[$index] = new Token([\T_DOUBLE_ARROW, $tokenContent]);
$newLineFoundSinceLastPlaceholder = false;
$yieldFoundSinceLastPlaceholder = false;
continue;
}
if ($token->equals(';')) {
++$this->deepestLevel;
++$this->currentLevel;
continue;
}
if ($token->equals(',')) {
for ($i = $index; $i < $endAt - 1; ++$i) {
if (str_contains($tokens[$i - 1]->getContent(), "\n")) {
$newLineFoundSinceLastPlaceholder = true;
break;
}
if ($tokens[$i + 1]->isGivenKind([\T_ARRAY, CT::T_ARRAY_SQUARE_BRACE_OPEN])) {
$arrayStartIndex = $tokens[$i + 1]->isGivenKind(\T_ARRAY)
? $tokens->getNextMeaningfulToken($i + 1)
: $i + 1;
$blockType = Tokens::detectBlockType($tokens[$arrayStartIndex]);
$arrayEndIndex = $tokens->findBlockEnd($blockType['type'], $arrayStartIndex);
if ($tokens->isPartialCodeMultiline($arrayStartIndex, $arrayEndIndex)) {
break;
}
}
++$index;
}
}
if ($token->equals('{')) {
$until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
$this->injectArrayAlignmentPlaceholders($tokens, $index + 1, $until - 1);
$index = $until;
continue;
}
if ($token->equals('(')) {
$until = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
$this->injectArrayAlignmentPlaceholders($tokens, $index + 1, $until - 1);
$index = $until;
continue;
}
}
}
private function injectArrayAlignmentPlaceholders(Tokens $tokens, int $from, int $until): void
{
// Only inject placeholders for multi-line arrays
if ($tokens->isPartialCodeMultiline($from, $until)) {
++$this->deepestLevel;
$currentLevel = $this->currentLevel;
$this->currentLevel = $this->deepestLevel;
$this->injectAlignmentPlaceholdersForArrow($tokens, $from, $until);
$this->currentLevel = $currentLevel;
}
}
private function fixWhiteSpaceBeforeOperator(Tokens $tokens, int $index, string $alignStrategy): void
{
// fix white space after operator is not needed as BinaryOperatorSpacesFixer took care of this (if strategy is _not_ ALIGN)
if (!$tokens[$index - 1]->isWhitespace()) {
$tokens->insertAt($index, new Token([\T_WHITESPACE, ' ']));
return;
}
if (
self::ALIGN_SINGLE_SPACE_MINIMAL !== $alignStrategy && self::ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE !== $alignStrategy
|| $tokens[$tokens->getPrevNonWhitespace($index - 1)]->isComment()
) {
return;
}
$content = $tokens[$index - 1]->getContent();
if (' ' !== $content && !str_contains($content, "\n")) {
$tokens[$index - 1] = new Token([\T_WHITESPACE, ' ']);
}
}
/**
* Look for group of placeholders and provide vertical alignment.
*/
private function replacePlaceholders(Tokens $tokens, string $alignStrategy, string $tokenContent): string
{
$tmpCode = $tokens->generateCode();
for ($j = 0; $j <= $this->deepestLevel; ++$j) {
$placeholder = \sprintf(self::ALIGN_PLACEHOLDER, $j);
if (!str_contains($tmpCode, $placeholder)) {
continue;
}
$lines = explode("\n", $tmpCode);
$groups = [];
$groupIndex = 0;
$groups[$groupIndex] = [];
foreach ($lines as $index => $line) {
if (substr_count($line, $placeholder) > 0) {
$groups[$groupIndex][] = $index;
} elseif (
self::ALIGN_BY_SCOPE !== $alignStrategy
&& self::ALIGN_SINGLE_SPACE_BY_SCOPE !== $alignStrategy
&& self::ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE !== $alignStrategy
) {
++$groupIndex;
$groups[$groupIndex] = [];
}
}
foreach ($groups as $group) {
if (\count($group) < 1) {
continue;
}
if (self::ALIGN !== $alignStrategy) {
// move placeholders to match strategy
foreach ($group as $index) {
$currentPosition = strpos($lines[$index], $placeholder);
$before = substr($lines[$index], 0, $currentPosition);
if (
self::ALIGN_SINGLE_SPACE === $alignStrategy
|| self::ALIGN_SINGLE_SPACE_BY_SCOPE === $alignStrategy
) {
if (!str_ends_with($before, ' ')) { // if last char of before-content is not ' '; add it
$before .= ' ';
}
} elseif (
self::ALIGN_SINGLE_SPACE_MINIMAL === $alignStrategy
|| self::ALIGN_SINGLE_SPACE_MINIMAL_BY_SCOPE === $alignStrategy
) {
if (!Preg::match('/^\h+$/', $before)) { // if indent; do not move, leave to other fixer
$before = rtrim($before).' ';
}
}
$lines[$index] = $before.substr($lines[$index], $currentPosition);
}
}
$rightmostSymbol = 0;
foreach ($group as $index) {
$rightmostSymbol = max($rightmostSymbol, $this->getSubstringWidth($lines[$index], $placeholder));
}
foreach ($group as $index) {
$line = $lines[$index];
$currentSymbol = $this->getSubstringWidth($line, $placeholder);
$delta = abs($rightmostSymbol - $currentSymbol);
if ($delta > 0) {
$line = str_replace($placeholder, str_repeat(' ', $delta).$placeholder, $line);
$lines[$index] = $line;
}
}
}
$tmpCode = str_replace($placeholder, '', implode("\n", $lines));
}
return $tmpCode;
}
private function getSubstringWidth(string $haystack, string $needle): int
{
$position = strpos($haystack, $needle);
\assert(\is_int($position));
$substring = substr($haystack, 0, $position);
return mb_strwidth($substring);
}
}

View File

@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\Operator;
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{
* spacing?: 'none'|'one',
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* spacing: 'none'|'one',
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class ConcatSpaceFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Concatenation should be spaced according to configuration.',
[
new CodeSample(
"<?php\n\$foo = 'bar' . 3 . 'baz'.'qux';\n"
),
new CodeSample(
"<?php\n\$foo = 'bar' . 3 . 'baz'.'qux';\n",
['spacing' => 'none']
),
new CodeSample(
"<?php\n\$foo = 'bar' . 3 . 'baz'.'qux';\n",
['spacing' => 'one']
),
]
);
}
/**
* {@inheritdoc}
*
* Must run after NoUnneededControlParenthesesFixer, SingleLineThrowFixer.
*/
public function getPriority(): int
{
return 0;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound('.');
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
for ($index = $tokens->count() - 1; $index >= 0; --$index) {
if ($tokens[$index]->equals('.')) {
if ('one' === $this->configuration['spacing']) {
$this->fixConcatenationToSingleSpace($tokens, $index);
} else {
$this->fixConcatenationToNoSpace($tokens, $index);
}
}
}
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('spacing', 'Spacing to apply around concatenation operator.'))
->setAllowedValues(['one', 'none'])
->setDefault('none')
->getOption(),
]);
}
/**
* @param int $index index of concatenation '.' token
*/
private function fixConcatenationToNoSpace(Tokens $tokens, int $index): void
{
$prevNonWhitespaceToken = $tokens[$tokens->getPrevNonWhitespace($index)];
if (!$prevNonWhitespaceToken->isGivenKind([\T_LNUMBER, \T_COMMENT, \T_DOC_COMMENT]) || str_starts_with($prevNonWhitespaceToken->getContent(), '/*')) {
$tokens->removeLeadingWhitespace($index, " \t");
}
if (!$tokens[$tokens->getNextNonWhitespace($index)]->isGivenKind([\T_LNUMBER, \T_COMMENT, \T_DOC_COMMENT])) {
$tokens->removeTrailingWhitespace($index, " \t");
}
}
/**
* @param int $index index of concatenation '.' token
*/
private function fixConcatenationToSingleSpace(Tokens $tokens, int $index): void
{
$this->fixWhiteSpaceAroundConcatToken($tokens, $index, 1);
$this->fixWhiteSpaceAroundConcatToken($tokens, $index, -1);
}
/**
* @param int $index index of concatenation '.' token
* @param int $offset 1 or -1
*/
private function fixWhiteSpaceAroundConcatToken(Tokens $tokens, int $index, int $offset): void
{
if (-1 !== $offset && 1 !== $offset) {
throw new \InvalidArgumentException(\sprintf(
'Expected `-1|1` for "$offset", got "%s"',
$offset
));
}
$offsetIndex = $index + $offset;
if (!$tokens[$offsetIndex]->isWhitespace()) {
$tokens->insertAt($index + (1 === $offset ? 1 : 0), new Token([\T_WHITESPACE, ' ']));
return;
}
if (str_contains($tokens[$offsetIndex]->getContent(), "\n")) {
return;
}
if ($tokens[$index + $offset * 2]->isComment()) {
return;
}
$tokens[$offsetIndex] = new Token([\T_WHITESPACE, ' ']);
}
}

View File

@@ -0,0 +1,179 @@
<?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\Operator;
use PhpCsFixer\Fixer\AbstractIncrementOperatorFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
/**
* @phpstan-type _AutogeneratedInputConfiguration array{
* style?: 'post'|'pre',
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* style: 'post'|'pre',
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Gregor Harlan <gharlan@web.de>
* @author Kuba Werłos <werlos@gmail.com>
*/
final class IncrementStyleFixer extends AbstractIncrementOperatorFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
/**
* @internal
*/
public const STYLE_PRE = 'pre';
/**
* @internal
*/
public const STYLE_POST = 'post';
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Pre- or post-increment and decrement operators should be used if possible.',
[
new CodeSample("<?php\n\$a++;\n\$b--;\n"),
new CodeSample(
"<?php\n++\$a;\n--\$b;\n",
['style' => self::STYLE_POST]
),
]
);
}
/**
* {@inheritdoc}
*
* Must run before NoSpacesInsideParenthesisFixer, SpacesInsideParenthesesFixer.
* Must run after StandardizeIncrementFixer.
*/
public function getPriority(): int
{
return 15;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound([\T_INC, \T_DEC]);
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('style', 'Whether to use pre- or post-increment and decrement operators.'))
->setAllowedValues([self::STYLE_PRE, self::STYLE_POST])
->setDefault(self::STYLE_PRE)
->getOption(),
]);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$tokensAnalyzer = new TokensAnalyzer($tokens);
for ($index = $tokens->count() - 1; 0 <= $index; --$index) {
$token = $tokens[$index];
if (!$token->isGivenKind([\T_INC, \T_DEC])) {
continue;
}
if (self::STYLE_PRE === $this->configuration['style'] && $tokensAnalyzer->isUnarySuccessorOperator($index)) {
$nextToken = $tokens[$tokens->getNextMeaningfulToken($index)];
if (!$nextToken->equalsAny([';', ')'])) {
continue;
}
$startIndex = $this->findStart($tokens, $index);
$prevToken = $tokens[$tokens->getPrevMeaningfulToken($startIndex)];
if ($prevToken->equalsAny([';', '{', '}', [\T_OPEN_TAG], ')'])) {
$tokens->clearAt($index);
$tokens->insertAt($startIndex, clone $token);
}
} elseif (self::STYLE_POST === $this->configuration['style'] && $tokensAnalyzer->isUnaryPredecessorOperator($index)) {
$prevToken = $tokens[$tokens->getPrevMeaningfulToken($index)];
if (!$prevToken->equalsAny([';', '{', '}', [\T_OPEN_TAG], ')'])) {
continue;
}
$endIndex = $this->findEnd($tokens, $index);
$nextToken = $tokens[$tokens->getNextMeaningfulToken($endIndex)];
if ($nextToken->equalsAny([';', ')'])) {
$tokens->clearAt($index);
$tokens->insertAt($tokens->getNextNonWhitespace($endIndex), clone $token);
}
}
}
}
private function findEnd(Tokens $tokens, int $index): int
{
$nextIndex = $tokens->getNextMeaningfulToken($index);
$nextToken = $tokens[$nextIndex];
while ($nextToken->equalsAny([
'$',
'(',
'[',
[CT::T_DYNAMIC_PROP_BRACE_OPEN],
[CT::T_DYNAMIC_VAR_BRACE_OPEN],
[CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN],
[\T_NS_SEPARATOR],
[\T_STATIC],
[\T_STRING],
[\T_VARIABLE],
])) {
$blockType = Tokens::detectBlockType($nextToken);
if (null !== $blockType) {
$nextIndex = $tokens->findBlockEnd($blockType['type'], $nextIndex);
}
$index = $nextIndex;
$nextIndex = $tokens->getNextMeaningfulToken($nextIndex);
$nextToken = $tokens[$nextIndex];
}
if ($nextToken->isObjectOperator()) {
return $this->findEnd($tokens, $nextIndex);
}
if ($nextToken->isGivenKind(\T_PAAMAYIM_NEKUDOTAYIM)) {
return $this->findEnd($tokens, $tokens->getNextMeaningfulToken($nextIndex));
}
return $index;
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\Operator;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Haralan Dobrev <hkdobrev@gmail.com>
*/
final class LogicalOperatorsFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Use `&&` and `||` logical operators instead of `and` and `or`.',
[
new CodeSample(
'<?php
if ($a == "foo" and ($b == "bar" or $c == "baz")) {
}
'
),
],
null,
'Risky, because you must double-check if using and/or with lower precedence was intentional.'
);
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound([\T_LOGICAL_AND, \T_LOGICAL_OR]);
}
public function isRisky(): bool
{
return true;
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
foreach ($tokens as $index => $token) {
if ($token->isGivenKind(\T_LOGICAL_AND)) {
$tokens[$index] = new Token([\T_BOOLEAN_AND, '&&']);
} elseif ($token->isGivenKind(\T_LOGICAL_OR)) {
$tokens[$index] = new Token([\T_BOOLEAN_OR, '||']);
}
}
}
}

View File

@@ -0,0 +1,136 @@
<?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\Operator;
use PhpCsFixer\Fixer\AbstractShortOperatorFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
final class LongToShorthandOperatorFixer extends AbstractShortOperatorFixer
{
/**
* @var array<string, array{int, string}>
*/
private const OPERATORS = [
'+' => [\T_PLUS_EQUAL, '+='],
'-' => [\T_MINUS_EQUAL, '-='],
'*' => [\T_MUL_EQUAL, '*='],
'/' => [\T_DIV_EQUAL, '/='],
'&' => [\T_AND_EQUAL, '&='],
'.' => [\T_CONCAT_EQUAL, '.='],
'%' => [\T_MOD_EQUAL, '%='],
'|' => [\T_OR_EQUAL, '|='],
'^' => [\T_XOR_EQUAL, '^='],
];
/**
* @var list<string>
*/
private array $operatorTypes;
private TokensAnalyzer $tokensAnalyzer;
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Shorthand notation for operators should be used if possible.',
[
new CodeSample("<?php\n\$i = \$i + 10;\n"),
],
null,
'Risky when applying for string offsets (e.g. `<?php $text = "foo"; $text[0] = $text[0] & "\x7F";`).',
);
}
/**
* {@inheritdoc}
*
* Must run before BinaryOperatorSpacesFixer, NoExtraBlankLinesFixer, NoSinglelineWhitespaceBeforeSemicolonsFixer, StandardizeIncrementFixer.
*/
public function getPriority(): int
{
return 17;
}
public function isRisky(): bool
{
return true;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound([...array_keys(self::OPERATORS), FCT::T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG, FCT::T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG]);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$this->operatorTypes = array_keys(self::OPERATORS);
$this->tokensAnalyzer = new TokensAnalyzer($tokens);
parent::applyFix($file, $tokens);
}
protected function isOperatorTokenCandidate(Tokens $tokens, int $index): bool
{
if (!$tokens[$index]->equalsAny($this->operatorTypes)) {
return false;
}
while (null !== $index) {
$index = $tokens->getNextMeaningfulToken($index);
$otherToken = $tokens[$index];
if ($otherToken->equalsAny([';', [\T_CLOSE_TAG]])) {
return true;
}
// fast precedence check
if ($otherToken->equals('?') || $otherToken->isGivenKind(\T_INSTANCEOF)) {
return false;
}
$blockType = Tokens::detectBlockType($otherToken);
if (null !== $blockType) {
if (false === $blockType['isStart']) {
return true;
}
$index = $tokens->findBlockEnd($blockType['type'], $index);
continue;
}
// precedence check
if ($this->tokensAnalyzer->isBinaryOperator($index)) {
return false;
}
}
return false; // unreachable, but keeps SCA happy
}
protected function getReplacementToken(Token $token): Token
{
\assert(isset(self::OPERATORS[$token->getContent()])); // for PHPStan
return new Token(self::OPERATORS[$token->getContent()]);
}
}

View File

@@ -0,0 +1,239 @@
<?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\Operator;
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\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;
/**
* @phpstan-type _AutogeneratedInputConfiguration array{
* use_parentheses?: bool,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* use_parentheses: bool,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Valentin Udaltsov <udaltsov.valentin@gmail.com>
*/
final class NewExpressionParenthesesFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'All `new` expressions with a further call must (not) be wrapped in parentheses.',
[
new VersionSpecificCodeSample(
"<?php\n\n(new Foo())->bar();\n",
new VersionSpecification(8_04_00)
),
new VersionSpecificCodeSample(
"<?php\n\n(new class {})->bar();\n",
new VersionSpecification(8_04_00)
),
new VersionSpecificCodeSample(
"<?php\n\nnew Foo()->bar();\n",
new VersionSpecification(8_04_00),
['use_parentheses' => true]
),
new VersionSpecificCodeSample(
"<?php\n\nnew class {}->bar();\n",
new VersionSpecification(8_04_00),
['use_parentheses' => true]
),
]
);
}
/**
* {@inheritdoc}
*
* Must run after NewWithParenthesesFixer, NoUnneededControlParenthesesFixer.
*/
public function getPriority(): int
{
return 29;
}
public function isCandidate(Tokens $tokens): bool
{
return \PHP_VERSION_ID >= 8_04_00 && $tokens->isTokenKindFound(\T_NEW);
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('use_parentheses', 'Whether `new` expressions with a further call should be wrapped in parentheses or not.'))
->setAllowedTypes(['bool'])
->setDefault(false)
->getOption(),
]);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$useParentheses = $this->configuration['use_parentheses'];
for ($index = $tokens->count() - 3; $index > 0; --$index) {
if (!$tokens[$index]->isGivenKind(\T_NEW)) {
continue;
}
$classStartIndex = $tokens->getNextMeaningfulToken($index);
if (null === $classStartIndex) {
return;
}
// anonymous class
if ($tokens[$classStartIndex]->isGivenKind(\T_CLASS)) {
$nextIndex = $tokens->getNextMeaningfulToken($classStartIndex);
if ($tokens[$nextIndex]->equals('(')) {
$nextIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $nextIndex);
} else {
$nextIndex = $classStartIndex;
}
$bodyStartIndex = $tokens->getNextTokenOfKind($nextIndex, ['{']);
$bodyEndIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $bodyStartIndex);
if ($useParentheses) {
$this->ensureWrappedInParentheses($tokens, $index, $bodyEndIndex);
} else {
$this->ensureNotWrappedInParentheses($tokens, $index, $bodyEndIndex);
}
continue;
}
// named class
$classEndIndex = $this->findClassEndIndex($tokens, $classStartIndex);
if (null === $classEndIndex) {
continue;
}
$nextIndex = $tokens->getNextMeaningfulToken($classEndIndex);
if (!$tokens[$nextIndex]->equals('(')) {
// If arguments' parentheses are absent then either this new expression is not further called
// and does not need parentheses, or we cannot omit its parentheses due to the grammar rules.
continue;
}
$argsEndIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $nextIndex);
if ($useParentheses) {
$this->ensureWrappedInParentheses($tokens, $index, $argsEndIndex);
} else {
$this->ensureNotWrappedInParentheses($tokens, $index, $argsEndIndex);
}
}
}
private function ensureWrappedInParentheses(Tokens $tokens, int $exprStartIndex, int $exprEndIndex): void
{
$prevIndex = $tokens->getPrevMeaningfulToken($exprStartIndex);
$nextIndex = $tokens->getNextMeaningfulToken($exprEndIndex);
if ($tokens[$prevIndex]->isGivenKind(CT::T_BRACE_CLASS_INSTANTIATION_OPEN)
&& $tokens[$nextIndex]->isGivenKind(CT::T_BRACE_CLASS_INSTANTIATION_CLOSE)
) {
return;
}
if (!$tokens[$nextIndex]->isObjectOperator() && !$tokens[$nextIndex]->isGivenKind(\T_PAAMAYIM_NEKUDOTAYIM)) {
return;
}
$tokens->insertAt($exprStartIndex, [new Token([CT::T_BRACE_CLASS_INSTANTIATION_OPEN, '('])]);
$tokens->insertAt($exprEndIndex + 2, [new Token([CT::T_BRACE_CLASS_INSTANTIATION_CLOSE, ')'])]);
}
private function ensureNotWrappedInParentheses(Tokens $tokens, int $exprStartIndex, int $exprEndIndex): void
{
$prevIndex = $tokens->getPrevMeaningfulToken($exprStartIndex);
$nextIndex = $tokens->getNextMeaningfulToken($exprEndIndex);
if (!$tokens[$prevIndex]->isGivenKind(CT::T_BRACE_CLASS_INSTANTIATION_OPEN)
|| !$tokens[$nextIndex]->isGivenKind(CT::T_BRACE_CLASS_INSTANTIATION_CLOSE)
) {
return;
}
$operatorIndex = $tokens->getNextMeaningfulToken($nextIndex);
if (!$tokens[$operatorIndex]->isObjectOperator() && !$tokens[$operatorIndex]->isGivenKind(\T_PAAMAYIM_NEKUDOTAYIM)) {
return;
}
$tokens->clearTokenAndMergeSurroundingWhitespace($prevIndex);
$tokens->clearTokenAndMergeSurroundingWhitespace($nextIndex);
}
private function findClassEndIndex(Tokens $tokens, int $index): ?int
{
// (expression) class name
if ($tokens[$index]->equals('(')) {
return $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
}
// regular class name or $variable class name
$nextTokens = [
[\T_STRING],
[\T_NS_SEPARATOR],
[CT::T_NAMESPACE_OPERATOR],
[\T_VARIABLE],
'$',
[CT::T_DYNAMIC_VAR_BRACE_OPEN],
'[',
[\T_OBJECT_OPERATOR],
[\T_NULLSAFE_OBJECT_OPERATOR],
[\T_PAAMAYIM_NEKUDOTAYIM],
];
if (!$tokens[$index]->equalsAny($nextTokens)) {
return null;
}
while ($tokens[$index]->equalsAny($nextTokens)) {
$blockType = Tokens::detectBlockType($tokens[$index]);
if (null !== $blockType) {
$index = $tokens->findBlockEnd($blockType['type'], $index);
}
$index = $tokens->getNextMeaningfulToken($index);
}
return $index - 1;
}
}

View File

@@ -0,0 +1,103 @@
<?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\Operator;
use PhpCsFixer\AbstractProxyFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
use PhpCsFixer\Fixer\DeprecatedFixerInterface;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
/**
* @deprecated
*
* @phpstan-type _AutogeneratedInputConfiguration array{
* anonymous_class?: bool,
* named_class?: bool,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* anonymous_class: bool,
* named_class: bool,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class NewWithBracesFixer extends AbstractProxyFixer implements ConfigurableFixerInterface, DeprecatedFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
private NewWithParenthesesFixer $newWithParenthesesFixer;
public function __construct()
{
$this->newWithParenthesesFixer = new NewWithParenthesesFixer();
parent::__construct();
}
public function getDefinition(): FixerDefinitionInterface
{
$fixerDefinition = $this->newWithParenthesesFixer->getDefinition();
return new FixerDefinition(
'All instances created with `new` keyword must (not) be followed by braces.',
$fixerDefinition->getCodeSamples(),
$fixerDefinition->getDescription(),
$fixerDefinition->getRiskyDescription(),
);
}
/**
* {@inheritdoc}
*
* Must run before ClassDefinitionFixer.
*/
public function getPriority(): int
{
return $this->newWithParenthesesFixer->getPriority();
}
public function getSuccessorsNames(): array
{
return [
$this->newWithParenthesesFixer->getName(),
];
}
/**
* @param _AutogeneratedInputConfiguration $configuration
*/
protected function configurePreNormalisation(array $configuration): void
{
$this->newWithParenthesesFixer->configure($configuration);
}
protected function createProxyFixers(): array
{
return [
$this->newWithParenthesesFixer,
];
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return $this->newWithParenthesesFixer->createConfigurationDefinition();
}
}

View File

@@ -0,0 +1,216 @@
<?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\Operator;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @phpstan-type _AutogeneratedInputConfiguration array{
* anonymous_class?: bool,
* named_class?: bool,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* anonymous_class: bool,
* named_class: bool,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class NewWithParenthesesFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
private const NEXT_TOKEN_KINDS = [
'?',
';',
',',
'(',
')',
'[',
']',
':',
'<',
'>',
'+',
'-',
'*',
'/',
'%',
'&',
'^',
'|',
[\T_CLASS],
[\T_IS_SMALLER_OR_EQUAL],
[\T_IS_GREATER_OR_EQUAL],
[\T_IS_EQUAL],
[\T_IS_NOT_EQUAL],
[\T_IS_IDENTICAL],
[\T_IS_NOT_IDENTICAL],
[\T_CLOSE_TAG],
[\T_LOGICAL_AND],
[\T_LOGICAL_OR],
[\T_LOGICAL_XOR],
[\T_BOOLEAN_AND],
[\T_BOOLEAN_OR],
[\T_SL],
[\T_SR],
[\T_INSTANCEOF],
[\T_AS],
[\T_DOUBLE_ARROW],
[\T_POW],
[\T_SPACESHIP],
[CT::T_ARRAY_SQUARE_BRACE_OPEN],
[CT::T_ARRAY_SQUARE_BRACE_CLOSE],
[CT::T_BRACE_CLASS_INSTANTIATION_OPEN],
[CT::T_BRACE_CLASS_INSTANTIATION_CLOSE],
[FCT::T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG],
[FCT::T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG],
];
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'All instances created with `new` keyword must (not) be followed by parentheses.',
[
new CodeSample("<?php\n\n\$x = new X;\n\$y = new class {};\n"),
new CodeSample(
"<?php\n\n\$y = new class() {};\n",
['anonymous_class' => false]
),
new CodeSample(
"<?php\n\n\$x = new X();\n",
['named_class' => false]
),
]
);
}
/**
* {@inheritdoc}
*
* Must run before ClassDefinitionFixer, NewExpressionParenthesesFixer.
*/
public function getPriority(): int
{
return 38;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(\T_NEW);
}
/** @protected */
public function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('named_class', 'Whether named classes should be followed by parentheses.'))
->setAllowedTypes(['bool'])
->setDefault(true)
->getOption(),
(new FixerOptionBuilder('anonymous_class', 'Whether anonymous classes should be followed by parentheses.'))
->setAllowedTypes(['bool'])
->setDefault(true) // @TODO 4.0: set to `false`
->getOption(),
]);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
for ($index = $tokens->count() - 3; $index > 0; --$index) {
if (!$tokens[$index]->isGivenKind(\T_NEW)) {
continue;
}
$nextIndex = $tokens->getNextTokenOfKind($index, self::NEXT_TOKEN_KINDS);
// new anonymous class definition
if ($tokens[$nextIndex]->isGivenKind(\T_CLASS)) {
$nextIndex = $tokens->getNextMeaningfulToken($nextIndex);
if (true === $this->configuration['anonymous_class']) {
$this->ensureParenthesesAt($tokens, $nextIndex);
} else {
$this->ensureNoParenthesesAt($tokens, $nextIndex);
}
continue;
}
// entrance into array index syntax - need to look for exit
while ($tokens[$nextIndex]->equals('[') || $tokens[$nextIndex]->isGivenKind(CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN)) {
$nextIndex = $tokens->findBlockEnd(Tokens::detectBlockType($tokens[$nextIndex])['type'], $nextIndex);
$nextIndex = $tokens->getNextMeaningfulToken($nextIndex);
}
if (true === $this->configuration['named_class']) {
$this->ensureParenthesesAt($tokens, $nextIndex);
} else {
$this->ensureNoParenthesesAt($tokens, $nextIndex);
}
}
}
private function ensureParenthesesAt(Tokens $tokens, int $index): void
{
$token = $tokens[$index];
if (!$token->equals('(') && !$token->isObjectOperator()) {
$tokens->insertAt(
$tokens->getPrevMeaningfulToken($index) + 1,
[new Token('('), new Token(')')]
);
}
}
private function ensureNoParenthesesAt(Tokens $tokens, int $index): void
{
if (!$tokens[$index]->equals('(')) {
return;
}
$closingIndex = $tokens->getNextMeaningfulToken($index);
// constructor has arguments - parentheses can not be removed
if (!$tokens[$closingIndex]->equals(')')) {
return;
}
// Check if there's an object operator after the closing parenthesis
// Preserve parentheses in expressions like "new A()->method()" as per RFC
$afterClosingIndex = $tokens->getNextMeaningfulToken($closingIndex);
if ($tokens[$afterClosingIndex]->isObjectOperator()) {
return;
}
$tokens->clearTokenAndMergeSurroundingWhitespace($closingIndex);
$tokens->clearTokenAndMergeSurroundingWhitespace($index);
}
}

View File

@@ -0,0 +1,73 @@
<?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\Operator;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Tokens;
final class NoSpaceAroundDoubleColonFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'There must be no space around double colons (also called Scope Resolution Operator or Paamayim Nekudotayim).',
[new CodeSample("<?php\n\necho Foo\\Bar :: class;\n")]
);
}
/**
* {@inheritdoc}
*
* Must run before MethodChainingIndentationFixer.
*/
public function getPriority(): int
{
return 1;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(\T_DOUBLE_COLON);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
for ($index = \count($tokens) - 2; $index > 1; --$index) {
if ($tokens[$index]->isGivenKind(\T_DOUBLE_COLON)) {
$this->removeSpace($tokens, $index, 1);
$this->removeSpace($tokens, $index, -1);
}
}
}
/**
* @param -1|1 $direction
*/
private function removeSpace(Tokens $tokens, int $index, int $direction): void
{
if (!$tokens[$index + $direction]->isWhitespace()) {
return;
}
if ($tokens[$tokens->getNonWhitespaceSibling($index, $direction)]->isComment()) {
return;
}
$tokens->clearAt($index + $direction);
}
}

View File

@@ -0,0 +1,374 @@
<?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\Operator;
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\Preg;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @phpstan-type _ConcatOperandType array{
* start: int,
* end: int,
* type: self::STR_*,
* }
* @phpstan-type _AutogeneratedInputConfiguration array{
* juggle_simple_strings?: bool,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* juggle_simple_strings: bool,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*/
final class NoUselessConcatOperatorFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
private const STR_DOUBLE_QUOTE = 0;
private const STR_DOUBLE_QUOTE_VAR = 1;
private const STR_SINGLE_QUOTE = 2;
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'There should not be useless concat operations.',
[
new CodeSample("<?php\n\$a = 'a'.'b';\n"),
new CodeSample("<?php\n\$a = 'a'.\"b\";\n", ['juggle_simple_strings' => true]),
],
);
}
/**
* {@inheritdoc}
*
* Must run before DateTimeCreateFromFormatCallFixer, EregToPregFixer, PhpUnitDedicateAssertInternalTypeFixer, RegularCallableCallFixer, SetTypeToCastFixer.
* Must run after ExplicitStringVariableFixer, NoBinaryStringFixer, SingleQuoteFixer.
*/
public function getPriority(): int
{
return 5;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound('.') && $tokens->isAnyTokenKindsFound([\T_CONSTANT_ENCAPSED_STRING, '"']);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
for ($index = $tokens->count() - 1; $index > 0; --$index) {
if (!$tokens[$index]->equals('.')) {
continue;
}
$nextMeaningfulTokenIndex = $tokens->getNextMeaningfulToken($index);
if ($this->containsLinebreak($tokens, $index, $nextMeaningfulTokenIndex)) {
continue;
}
$secondOperand = $this->getConcatOperandType($tokens, $nextMeaningfulTokenIndex, 1);
if (null === $secondOperand) {
continue;
}
$prevMeaningfulTokenIndex = $tokens->getPrevMeaningfulToken($index);
if ($this->containsLinebreak($tokens, $prevMeaningfulTokenIndex, $index)) {
continue;
}
$firstOperand = $this->getConcatOperandType($tokens, $prevMeaningfulTokenIndex, -1);
if (null === $firstOperand) {
continue;
}
$this->fixConcatOperation($tokens, $firstOperand, $index, $secondOperand);
}
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('juggle_simple_strings', 'Allow for simple string quote juggling if it results in more concat-operations merges.'))
->setAllowedTypes(['bool'])
->setDefault(false)
->getOption(),
]);
}
/**
* @param _ConcatOperandType $firstOperand
* @param _ConcatOperandType $secondOperand
*/
private function fixConcatOperation(Tokens $tokens, array $firstOperand, int $concatIndex, array $secondOperand): void
{
// if both operands are of the same type then these operands can always be merged
if (
(self::STR_DOUBLE_QUOTE === $firstOperand['type'] && self::STR_DOUBLE_QUOTE === $secondOperand['type'])
|| (self::STR_SINGLE_QUOTE === $firstOperand['type'] && self::STR_SINGLE_QUOTE === $secondOperand['type'])
) {
$this->mergeConstantEscapedStringOperands($tokens, $firstOperand, $concatIndex, $secondOperand);
return;
}
if (self::STR_DOUBLE_QUOTE_VAR === $firstOperand['type'] && self::STR_DOUBLE_QUOTE_VAR === $secondOperand['type']) {
if ($this->operandsCanNotBeMerged($tokens, $firstOperand, $secondOperand)) {
return;
}
$this->mergeConstantEscapedStringVarOperands($tokens, $firstOperand, $concatIndex, $secondOperand);
return;
}
// if any is double and the other is not, check for simple other, than merge with "
$operands = [
[$firstOperand, $secondOperand],
[$secondOperand, $firstOperand],
];
foreach ($operands as $operandPair) {
[$operand1, $operand2] = $operandPair;
if (self::STR_DOUBLE_QUOTE_VAR === $operand1['type'] && self::STR_DOUBLE_QUOTE === $operand2['type']) {
if ($this->operandsCanNotBeMerged($tokens, $operand1, $operand2)) {
return;
}
$this->mergeConstantEscapedStringVarOperands($tokens, $firstOperand, $concatIndex, $secondOperand);
return;
}
if (false === $this->configuration['juggle_simple_strings']) {
continue;
}
if (self::STR_DOUBLE_QUOTE === $operand1['type'] && self::STR_SINGLE_QUOTE === $operand2['type']) {
$operantContent = $tokens[$operand2['start']]->getContent();
if ($this->isSimpleQuotedStringContent($operantContent)) {
$this->mergeConstantEscapedStringOperands($tokens, $firstOperand, $concatIndex, $secondOperand);
}
return;
}
if (self::STR_DOUBLE_QUOTE_VAR === $operand1['type'] && self::STR_SINGLE_QUOTE === $operand2['type']) {
$operantContent = $tokens[$operand2['start']]->getContent();
if ($this->isSimpleQuotedStringContent($operantContent)) {
if ($this->operandsCanNotBeMerged($tokens, $operand1, $operand2)) {
return;
}
$this->mergeConstantEscapedStringVarOperands($tokens, $firstOperand, $concatIndex, $secondOperand);
}
return;
}
}
}
/**
* @param -1|1 $direction
*
* @return null|_ConcatOperandType
*/
private function getConcatOperandType(Tokens $tokens, int $index, int $direction): ?array
{
if ($tokens[$index]->isGivenKind(\T_CONSTANT_ENCAPSED_STRING)) {
$firstChar = $tokens[$index]->getContent();
if ('b' === $firstChar[0] || 'B' === $firstChar[0]) {
return null; // we don't care about these, priorities are set to do deal with these cases
}
return [
'start' => $index,
'end' => $index,
'type' => '"' === $firstChar[0] ? self::STR_DOUBLE_QUOTE : self::STR_SINGLE_QUOTE,
];
}
if ($tokens[$index]->equals('"')) {
$end = $tokens->getTokenOfKindSibling($index, $direction, ['"']);
return [
'start' => 1 === $direction ? $index : $end,
'end' => 1 === $direction ? $end : $index,
'type' => self::STR_DOUBLE_QUOTE_VAR,
];
}
return null;
}
/**
* @param _ConcatOperandType $firstOperand
* @param _ConcatOperandType $secondOperand
*/
private function mergeConstantEscapedStringOperands(
Tokens $tokens,
array $firstOperand,
int $concatOperatorIndex,
array $secondOperand
): void {
$quote = self::STR_DOUBLE_QUOTE === $firstOperand['type'] || self::STR_DOUBLE_QUOTE === $secondOperand['type'] ? '"' : "'";
$firstOperandTokenContent = $tokens[$firstOperand['start']]->getContent();
$secondOperandTokenContent = $tokens[$secondOperand['start']]->getContent();
$tokens[$firstOperand['start']] = new Token(
[
\T_CONSTANT_ENCAPSED_STRING,
$quote.substr($firstOperandTokenContent, 1, -1).substr($secondOperandTokenContent, 1, -1).$quote,
],
);
$this->clearConcatAndAround($tokens, $concatOperatorIndex);
$tokens->clearTokenAndMergeSurroundingWhitespace($secondOperand['start']);
}
/**
* @param _ConcatOperandType $firstOperand
* @param _ConcatOperandType $secondOperand
*/
private function mergeConstantEscapedStringVarOperands(
Tokens $tokens,
array $firstOperand,
int $concatOperatorIndex,
array $secondOperand
): void {
// build up the new content
$newContent = '';
foreach ([$firstOperand, $secondOperand] as $operant) {
$operandContent = '';
for ($i = $operant['start']; $i <= $operant['end'];) {
$operandContent .= $tokens[$i]->getContent();
$i = $tokens->getNextMeaningfulToken($i);
}
$newContent .= substr($operandContent, 1, -1);
}
// remove tokens making up the concat statement
for ($i = $secondOperand['end']; $i >= $secondOperand['start'];) {
$tokens->clearTokenAndMergeSurroundingWhitespace($i);
$i = $tokens->getPrevMeaningfulToken($i);
}
$this->clearConcatAndAround($tokens, $concatOperatorIndex);
for ($i = $firstOperand['end']; $i > $firstOperand['start'];) {
$tokens->clearTokenAndMergeSurroundingWhitespace($i);
$i = $tokens->getPrevMeaningfulToken($i);
}
// insert new tokens based on the new content
$newTokens = Tokens::fromCode('<?php "'.$newContent.'";');
$newTokensCount = \count($newTokens);
$insertTokens = [];
for ($i = 1; $i < $newTokensCount - 1; ++$i) {
$insertTokens[] = $newTokens[$i];
}
$tokens->overrideRange($firstOperand['start'], $firstOperand['start'], $insertTokens);
}
private function clearConcatAndAround(Tokens $tokens, int $concatOperatorIndex): void
{
if ($tokens[$concatOperatorIndex + 1]->isWhitespace()) {
$tokens->clearTokenAndMergeSurroundingWhitespace($concatOperatorIndex + 1);
}
$tokens->clearTokenAndMergeSurroundingWhitespace($concatOperatorIndex);
if ($tokens[$concatOperatorIndex - 1]->isWhitespace()) {
$tokens->clearTokenAndMergeSurroundingWhitespace($concatOperatorIndex - 1);
}
}
private function isSimpleQuotedStringContent(string $candidate): bool
{
return !Preg::match('#[\$"\'\\\]#', substr($candidate, 1, -1));
}
private function containsLinebreak(Tokens $tokens, int $startIndex, int $endIndex): bool
{
for ($i = $endIndex; $i > $startIndex; --$i) {
if (Preg::match('/\R/', $tokens[$i]->getContent())) {
return true;
}
}
return false;
}
/**
* @param _ConcatOperandType $firstOperand
* @param _ConcatOperandType $secondOperand
*/
private function operandsCanNotBeMerged(Tokens $tokens, array $firstOperand, array $secondOperand): bool
{
// If the first operand does not end with a variable, no variables would be broken by concatenation.
if (self::STR_DOUBLE_QUOTE_VAR !== $firstOperand['type']) {
return false;
}
if (!$tokens[$firstOperand['end'] - 1]->isGivenKind(\T_VARIABLE)) {
return false;
}
$allowedPatternsForSecondOperand = [
'/^ .*/', // e.g. " foo", ' bar', " $baz"
'/^-(?!\>)/', // e.g. "-foo", '-bar', "-$baz"
];
// If the first operand ends with a variable, the second operand should match one of the allowed patterns.
// Otherwise, the concatenation can break a variable in the first operand.
foreach ($allowedPatternsForSecondOperand as $allowedPattern) {
$secondOperandInnerContent = substr($tokens->generatePartialCode($secondOperand['start'], $secondOperand['end']), 1, -1);
if (Preg::match($allowedPattern, $secondOperandInnerContent)) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,73 @@
<?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\Operator;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\FixerDefinition\VersionSpecification;
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
final class NoUselessNullsafeOperatorFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'There should not be useless Null-safe operator `?->` used.',
[
new VersionSpecificCodeSample(
'<?php
class Foo extends Bar
{
public function test() {
echo $this?->parentMethod();
}
}
',
new VersionSpecification(8_00_00)
),
]
);
}
public function isCandidate(Tokens $tokens): bool
{
return \PHP_VERSION_ID >= 8_00_00 && $tokens->isAllTokenKindsFound([\T_VARIABLE, \T_NULLSAFE_OBJECT_OPERATOR]);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
for ($index = $tokens->count() - 1; $index >= 0; --$index) {
if (!$tokens[$index]->isGivenKind(\T_NULLSAFE_OBJECT_OPERATOR)) {
continue;
}
$nullsafeObjectOperatorIndex = $index;
$index = $tokens->getPrevMeaningfulToken($index);
if (!$tokens[$index]->isGivenKind(\T_VARIABLE)) {
continue;
}
if ('$this' !== strtolower($tokens[$index]->getContent())) {
continue;
}
$tokens[$nullsafeObjectOperatorIndex] = new Token([\T_OBJECT_OPERATOR, '->']);
}
}
}

View File

@@ -0,0 +1,75 @@
<?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\Operator;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Javier Spagnoletti <phansys@gmail.com>
*/
final class NotOperatorWithSpaceFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Logical NOT operators (`!`) should have leading and trailing whitespaces.',
[new CodeSample(
'<?php
if (!$bar) {
echo "Help!";
}
'
)]
);
}
/**
* {@inheritdoc}
*
* Must run after ModernizeStrposFixer, UnaryOperatorSpacesFixer.
*/
public function getPriority(): int
{
return -10;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound('!');
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
for ($index = $tokens->count() - 1; $index >= 0; --$index) {
$token = $tokens[$index];
if ($token->equals('!')) {
if (!$tokens[$index + 1]->isWhitespace()) {
$tokens->insertAt($index + 1, new Token([\T_WHITESPACE, ' ']));
}
if (!$tokens[$index - 1]->isWhitespace()) {
$tokens->insertAt($index, new Token([\T_WHITESPACE, ' ']));
}
}
}
}
}

View File

@@ -0,0 +1,68 @@
<?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\Operator;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Javier Spagnoletti <phansys@gmail.com>
*/
final class NotOperatorWithSuccessorSpaceFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Logical NOT operators (`!`) should have one trailing whitespace.',
[new CodeSample(
'<?php
if (!$bar) {
echo "Help!";
}
'
)]
);
}
/**
* {@inheritdoc}
*
* Must run after ModernizeStrposFixer, UnaryOperatorSpacesFixer.
*/
public function getPriority(): int
{
return -10;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound('!');
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
for ($index = $tokens->count() - 1; $index >= 0; --$index) {
$token = $tokens[$index];
if ($token->equals('!')) {
$tokens->ensureWhitespaceAtIndex($index + 1, 0, ' ');
}
}
}
}

View File

@@ -0,0 +1,62 @@
<?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\Operator;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class ObjectOperatorWithoutWhitespaceFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'There should not be space before or after object operators `->` and `?->`.',
[new CodeSample("<?php \$a -> b;\n")]
);
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound(Token::getObjectOperatorKinds());
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
// [Structure] there should not be space before or after "->" or "?->"
foreach ($tokens as $index => $token) {
if (!$token->isObjectOperator()) {
continue;
}
// clear whitespace before ->
if ($tokens[$index - 1]->isWhitespace(" \t") && !$tokens[$index - 2]->isComment()) {
$tokens->clearAt($index - 1);
}
// clear whitespace after ->
if ($tokens[$index + 1]->isWhitespace(" \t") && !$tokens[$index + 2]->isComment()) {
$tokens->clearAt($index + 1);
}
}
}
}

View File

@@ -0,0 +1,304 @@
<?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\Operator;
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\Preg;
use PhpCsFixer\Tokenizer\Analyzer\AlternativeSyntaxAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\GotoLabelAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\ReferenceAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\SwitchAnalyzer;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @phpstan-type _AutogeneratedInputConfiguration array{
* only_booleans?: bool,
* position?: 'beginning'|'end',
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* only_booleans: bool,
* position: 'beginning'|'end',
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Kuba Werłos <werlos@gmail.com>
*/
final class OperatorLinebreakFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
private const BOOLEAN_OPERATORS = [[\T_BOOLEAN_AND], [\T_BOOLEAN_OR], [\T_LOGICAL_AND], [\T_LOGICAL_OR], [\T_LOGICAL_XOR]];
private string $position = 'beginning';
/**
* @var list<array{int}|string>
*/
private array $operators = [];
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Operators - when multiline - must always be at the beginning or at the end of the line.',
[
new CodeSample(
'<?php
$a = $b ||
$c;
$d = $e +
$f;
',
),
new CodeSample(
'<?php
$a = $b ||
$c;
$d = $e +
$f;
',
['only_booleans' => true]
),
new CodeSample(
'<?php
$a = $b
|| $c;
$d = $e
+ $f;
',
['position' => 'end']
),
],
);
}
public function isCandidate(Tokens $tokens): bool
{
return true;
}
protected function configurePostNormalisation(): void
{
$this->position = $this->configuration['position'];
$this->operators = self::BOOLEAN_OPERATORS;
if (false === $this->configuration['only_booleans']) {
$this->operators = array_merge($this->operators, self::getNonBooleanOperators());
}
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('only_booleans', 'Whether to limit operators to only boolean ones.'))
->setAllowedTypes(['bool'])
->setDefault(false)
->getOption(),
(new FixerOptionBuilder('position', 'Whether to place operators at the beginning or at the end of the line.'))
->setAllowedValues(['beginning', 'end'])
->setDefault($this->position)
->getOption(),
]);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$referenceAnalyzer = new ReferenceAnalyzer();
$gotoLabelAnalyzer = new GotoLabelAnalyzer();
$alternativeSyntaxAnalyzer = new AlternativeSyntaxAnalyzer();
$index = $tokens->count();
while ($index > 1) {
--$index;
if (!$tokens[$index]->equalsAny($this->operators, false)) {
continue;
}
if ($gotoLabelAnalyzer->belongsToGoToLabel($tokens, $index)) {
continue;
}
if ($referenceAnalyzer->isReference($tokens, $index)) {
continue;
}
if ($alternativeSyntaxAnalyzer->belongsToAlternativeSyntax($tokens, $index)) {
continue;
}
if (SwitchAnalyzer::belongsToSwitch($tokens, $index)) {
continue;
}
$operatorIndices = [$index];
if ($tokens[$index]->equals(':')) {
/** @var int $prevIndex */
$prevIndex = $tokens->getPrevMeaningfulToken($index);
if ($tokens[$prevIndex]->equals('?')) {
$operatorIndices = [$prevIndex, $index];
$index = $prevIndex;
}
}
$this->fixOperatorLinebreak($tokens, $operatorIndices);
}
}
/**
* @param non-empty-list<int> $operatorIndices
*/
private function fixOperatorLinebreak(Tokens $tokens, array $operatorIndices): void
{
/** @var int $prevIndex */
$prevIndex = $tokens->getPrevMeaningfulToken(min($operatorIndices));
$indexStart = $prevIndex + 1;
/** @var int $nextIndex */
$nextIndex = $tokens->getNextMeaningfulToken(max($operatorIndices));
$indexEnd = $nextIndex - 1;
if (!$this->isMultiline($tokens, $indexStart, $indexEnd)) {
return; // operator is not surrounded by multiline whitespaces, do not touch it
}
if ('beginning' === $this->position) {
if (!$this->isMultiline($tokens, max($operatorIndices), $indexEnd)) {
return; // operator already is placed correctly
}
$this->fixMoveToTheBeginning($tokens, $operatorIndices);
return;
}
if (!$this->isMultiline($tokens, $indexStart, min($operatorIndices))) {
return; // operator already is placed correctly
}
$this->fixMoveToTheEnd($tokens, $operatorIndices);
}
/**
* @param non-empty-list<int> $operatorIndices
*/
private function fixMoveToTheBeginning(Tokens $tokens, array $operatorIndices): void
{
/** @var int $prevIndex */
$prevIndex = $tokens->getNonEmptySibling(min($operatorIndices), -1);
/** @var int $nextIndex */
$nextIndex = $tokens->getNextMeaningfulToken(max($operatorIndices));
for ($i = $nextIndex - 1; $i > max($operatorIndices); --$i) {
if ($tokens[$i]->isWhitespace() && Preg::match('/\R/u', $tokens[$i]->getContent())) {
$isWhitespaceBefore = $tokens[$prevIndex]->isWhitespace();
$inserts = $this->getReplacementsAndClear($tokens, $operatorIndices, -1);
if ($isWhitespaceBefore) {
$inserts[] = new Token([\T_WHITESPACE, ' ']);
}
$tokens->insertAt($nextIndex, $inserts);
break;
}
}
}
/**
* @param non-empty-list<int> $operatorIndices
*/
private function fixMoveToTheEnd(Tokens $tokens, array $operatorIndices): void
{
/** @var int $prevIndex */
$prevIndex = $tokens->getPrevMeaningfulToken(min($operatorIndices));
/** @var int $nextIndex */
$nextIndex = $tokens->getNonEmptySibling(max($operatorIndices), 1);
for ($i = $prevIndex + 1; $i < max($operatorIndices); ++$i) {
if ($tokens[$i]->isWhitespace() && Preg::match('/\R/u', $tokens[$i]->getContent())) {
$isWhitespaceAfter = $tokens[$nextIndex]->isWhitespace();
$inserts = $this->getReplacementsAndClear($tokens, $operatorIndices, 1);
if ($isWhitespaceAfter) {
array_unshift($inserts, new Token([\T_WHITESPACE, ' ']));
}
$tokens->insertAt($prevIndex + 1, $inserts);
break;
}
}
}
/**
* @param list<int> $indices
*
* @return list<Token>
*/
private function getReplacementsAndClear(Tokens $tokens, array $indices, int $direction): array
{
return array_map(
static function (int $index) use ($tokens, $direction): Token {
$clone = $tokens[$index];
if ($tokens[$index + $direction]->isWhitespace()) {
$tokens->clearAt($index + $direction);
}
$tokens->clearAt($index);
return $clone;
},
$indices
);
}
private function isMultiline(Tokens $tokens, int $indexStart, int $indexEnd): bool
{
for ($index = $indexStart; $index <= $indexEnd; ++$index) {
if (str_contains($tokens[$index]->getContent(), "\n")) {
return true;
}
}
return false;
}
/**
* @return list<array{int}|string>
*/
private static function getNonBooleanOperators(): array
{
return array_merge(
[
'%', '&', '*', '+', '-', '.', '/', ':', '<', '=', '>', '?', '^', '|',
[\T_AND_EQUAL], [\T_CONCAT_EQUAL], [\T_DIV_EQUAL], [\T_DOUBLE_ARROW], [\T_IS_EQUAL], [\T_IS_GREATER_OR_EQUAL],
[\T_IS_IDENTICAL], [\T_IS_NOT_EQUAL], [\T_IS_NOT_IDENTICAL], [\T_IS_SMALLER_OR_EQUAL], [\T_MINUS_EQUAL],
[\T_MOD_EQUAL], [\T_MUL_EQUAL], [\T_OR_EQUAL], [\T_PAAMAYIM_NEKUDOTAYIM], [\T_PLUS_EQUAL], [\T_POW],
[\T_POW_EQUAL], [\T_SL], [\T_SL_EQUAL], [\T_SR], [\T_SR_EQUAL], [\T_XOR_EQUAL],
[\T_COALESCE], [\T_SPACESHIP], [FCT::T_PIPE],
],
array_map(static fn (int $id): array => [$id], Token::getObjectOperatorKinds()),
);
}
}

View File

@@ -0,0 +1,122 @@
<?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\Operator;
use PhpCsFixer\Fixer\AbstractIncrementOperatorFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author ntzm
*/
final class StandardizeIncrementFixer extends AbstractIncrementOperatorFixer
{
private const EXPRESSION_END_TOKENS = [
';',
')',
']',
',',
':',
[CT::T_DYNAMIC_PROP_BRACE_CLOSE],
[CT::T_DYNAMIC_VAR_BRACE_CLOSE],
[\T_CLOSE_TAG],
];
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Increment and decrement operators should be used if possible.',
[
new CodeSample("<?php\n\$i += 1;\n"),
new CodeSample("<?php\n\$i -= 1;\n"),
]
);
}
/**
* {@inheritdoc}
*
* Must run before IncrementStyleFixer.
* Must run after LongToShorthandOperatorFixer.
*/
public function getPriority(): int
{
return 16;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAnyTokenKindsFound([\T_PLUS_EQUAL, \T_MINUS_EQUAL]);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
for ($index = $tokens->count() - 1; $index > 0; --$index) {
$expressionEnd = $tokens[$index];
if (!$expressionEnd->equalsAny(self::EXPRESSION_END_TOKENS)) {
continue;
}
$numberIndex = $tokens->getPrevMeaningfulToken($index);
$number = $tokens[$numberIndex];
if (!$number->isGivenKind(\T_LNUMBER) || '1' !== $number->getContent()) {
continue;
}
$operatorIndex = $tokens->getPrevMeaningfulToken($numberIndex);
$operator = $tokens[$operatorIndex];
if (!$operator->isGivenKind([\T_PLUS_EQUAL, \T_MINUS_EQUAL])) {
continue;
}
$startIndex = $this->findStart($tokens, $operatorIndex);
$this->clearRangeLeaveComments(
$tokens,
$tokens->getPrevMeaningfulToken($operatorIndex) + 1,
$numberIndex
);
$tokens->insertAt(
$startIndex,
new Token($operator->isGivenKind(\T_PLUS_EQUAL) ? [\T_INC, '++'] : [\T_DEC, '--'])
);
}
}
/**
* Clear tokens in the given range unless they are comments.
*/
private function clearRangeLeaveComments(Tokens $tokens, int $indexStart, int $indexEnd): void
{
for ($i = $indexStart; $i <= $indexEnd; ++$i) {
$token = $tokens[$i];
if ($token->isComment()) {
continue;
}
if ($token->isWhitespace("\n\r")) {
continue;
}
$tokens->clearAt($i);
}
}
}

View File

@@ -0,0 +1,60 @@
<?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\Operator;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class StandardizeNotEqualsFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Replace all `<>` with `!=`.',
[new CodeSample("<?php\n\$a = \$b <> \$c;\n")]
);
}
/**
* {@inheritdoc}
*
* Must run before BinaryOperatorSpacesFixer.
*/
public function getPriority(): int
{
return 0;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(\T_IS_NOT_EQUAL);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
foreach ($tokens as $index => $token) {
if ($token->isGivenKind(\T_IS_NOT_EQUAL)) {
$tokens[$index] = new Token([\T_IS_NOT_EQUAL, '!=']);
}
}
}
}

View File

@@ -0,0 +1,131 @@
<?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\Operator;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Analyzer\AlternativeSyntaxAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\GotoLabelAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\SwitchAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class TernaryOperatorSpacesFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Standardize spaces around ternary operator.',
[new CodeSample("<?php \$a = \$a ?1 :0;\n")]
);
}
/**
* {@inheritdoc}
*
* Must run after ArraySyntaxFixer, ListSyntaxFixer, TernaryToElvisOperatorFixer.
*/
public function getPriority(): int
{
return 1;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAllTokenKindsFound(['?', ':']);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$alternativeSyntaxAnalyzer = new AlternativeSyntaxAnalyzer();
$gotoLabelAnalyzer = new GotoLabelAnalyzer();
$ternaryOperatorIndices = [];
foreach ($tokens as $index => $token) {
if (!$token->equalsAny(['?', ':'])) {
continue;
}
if (SwitchAnalyzer::belongsToSwitch($tokens, $index)) {
continue;
}
if ($alternativeSyntaxAnalyzer->belongsToAlternativeSyntax($tokens, $index)) {
continue;
}
if ($gotoLabelAnalyzer->belongsToGoToLabel($tokens, $index)) {
continue;
}
$ternaryOperatorIndices[] = $index;
}
foreach (array_reverse($ternaryOperatorIndices) as $index) {
$token = $tokens[$index];
if ($token->equals('?')) {
$nextNonWhitespaceIndex = $tokens->getNextNonWhitespace($index);
if ($tokens[$nextNonWhitespaceIndex]->equals(':')) {
// for `$a ?: $b` remove spaces between `?` and `:`
$tokens->ensureWhitespaceAtIndex($index + 1, 0, '');
} else {
// for `$a ? $b : $c` ensure space after `?`
$this->ensureWhitespaceExistence($tokens, $index + 1, true);
}
// for `$a ? $b : $c` ensure space before `?`
$this->ensureWhitespaceExistence($tokens, $index - 1, false);
continue;
}
if ($token->equals(':')) {
// for `$a ? $b : $c` ensure space after `:`
$this->ensureWhitespaceExistence($tokens, $index + 1, true);
$prevNonWhitespaceToken = $tokens[$tokens->getPrevNonWhitespace($index)];
if (!$prevNonWhitespaceToken->equals('?')) {
// for `$a ? $b : $c` ensure space before `:`
$this->ensureWhitespaceExistence($tokens, $index - 1, false);
}
}
}
}
private function ensureWhitespaceExistence(Tokens $tokens, int $index, bool $after): void
{
if ($tokens[$index]->isWhitespace()) {
if (
!str_contains($tokens[$index]->getContent(), "\n")
&& !$tokens[$index - 1]->isComment()
) {
$tokens[$index] = new Token([\T_WHITESPACE, ' ']);
}
return;
}
$index += $after ? 0 : 1;
$tokens->insertAt($index, new Token([\T_WHITESPACE, ' ']));
}
}

View File

@@ -0,0 +1,217 @@
<?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\Operator;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Analyzer\RangeAnalyzer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Tokens;
final class TernaryToElvisOperatorFixer extends AbstractFixer
{
/**
* Lower precedence and other valid preceding tokens.
*
* Ordered by most common types first.
*
* @var list<array{int}|string>
*/
private const VALID_BEFORE_ENDTYPES = [
'=',
[\T_OPEN_TAG],
[\T_OPEN_TAG_WITH_ECHO],
'(',
',',
';',
'[',
'{',
'}',
[CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN],
[\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], // ^=
];
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Use the Elvis operator `?:` where possible.',
[
new CodeSample(
"<?php\n\$foo = \$foo ? \$foo : 1;\n"
),
new CodeSample(
"<?php \$foo = \$bar[a()] ? \$bar[a()] : 1; # \"risky\" sample, \"a()\" only gets called once after fixing\n"
),
],
null,
'Risky when relying on functions called on both sides of the `?` operator.'
);
}
/**
* {@inheritdoc}
*
* Must run before NoTrailingWhitespaceFixer, TernaryOperatorSpacesFixer.
*/
public function getPriority(): int
{
return 2;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound('?');
}
public function isRisky(): bool
{
return true;
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
for ($index = \count($tokens) - 5; $index > 1; --$index) {
if (!$tokens[$index]->equals('?')) {
continue;
}
$nextIndex = $tokens->getNextMeaningfulToken($index);
if ($tokens[$nextIndex]->equals(':')) {
continue; // Elvis is alive!
}
// get and check what is before the `?` operator
$beforeOperator = $this->getBeforeOperator($tokens, $index);
if (null === $beforeOperator) {
continue; // contains something we cannot fix because of priorities
}
// get what is after the `?` token
$afterOperator = $this->getAfterOperator($tokens, $index);
// if before and after the `?` operator are the same (in meaningful matter), clear after
if (RangeAnalyzer::rangeEqualsRange($tokens, $beforeOperator, $afterOperator)) {
$this->clearMeaningfulFromRange($tokens, $afterOperator);
}
}
}
/**
* @return ?array{start: int, end: int} null if contains ++/-- operator
*/
private function getBeforeOperator(Tokens $tokens, int $index): ?array
{
$blockEdgeDefinitions = Tokens::getBlockEdgeDefinitions();
$index = $tokens->getPrevMeaningfulToken($index);
$before = ['end' => $index];
while (!$tokens[$index]->equalsAny(self::VALID_BEFORE_ENDTYPES)) {
if ($tokens[$index]->isGivenKind([\T_INC, \T_DEC])) {
return null;
}
$detectedBlockType = Tokens::detectBlockType($tokens[$index]);
if (null === $detectedBlockType || $detectedBlockType['isStart']) {
$before['start'] = $index;
$index = $tokens->getPrevMeaningfulToken($index);
continue;
}
/** @phpstan-ignore-next-line offsetAccess.notFound (we just detected block type, we know it's definition exists under given PHP runtime) */
$blockType = $blockEdgeDefinitions[$detectedBlockType['type']];
$openCount = 1;
do {
$index = $tokens->getPrevMeaningfulToken($index);
if ($tokens[$index]->isGivenKind([\T_INC, \T_DEC])) {
return null;
}
if ($tokens[$index]->equals($blockType['start'])) {
++$openCount;
continue;
}
if ($tokens[$index]->equals($blockType['end'])) {
--$openCount;
}
} while (1 >= $openCount);
$before['start'] = $index;
$index = $tokens->getPrevMeaningfulToken($index);
}
if (!isset($before['start'])) {
return null;
}
return $before;
}
/**
* @return array{start: int, end: int}
*/
private function getAfterOperator(Tokens $tokens, int $index): array
{
$index = $tokens->getNextMeaningfulToken($index);
$after = ['start' => $index];
do {
$blockType = Tokens::detectBlockType($tokens[$index]);
if (null !== $blockType) {
$index = $tokens->findBlockEnd($blockType['type'], $index);
}
$after['end'] = $index;
$index = $tokens->getNextMeaningfulToken($index);
} while (!$tokens[$index]->equals(':'));
return $after;
}
/**
* @param array{start: int, end: int} $range
*/
private function clearMeaningfulFromRange(Tokens $tokens, array $range): void
{
// $range['end'] must be meaningful!
for ($i = $range['end']; $i >= $range['start']; $i = $tokens->getPrevMeaningfulToken($i)) {
$tokens->clearTokenAndMergeSurroundingWhitespace($i);
}
}
}

View File

@@ -0,0 +1,220 @@
<?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\Operator;
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 TernaryToNullCoalescingFixer extends AbstractFixer
{
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Use `null` coalescing operator `??` where possible.',
[
new CodeSample(
"<?php\n\$sample = isset(\$a) ? \$a : \$b;\n"
),
]
);
}
/**
* {@inheritdoc}
*
* Must run before AssignNullCoalescingToCoalesceEqualFixer.
*/
public function getPriority(): int
{
return 0;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(\T_ISSET);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$issetIndices = array_keys($tokens->findGivenKind(\T_ISSET));
foreach (array_reverse($issetIndices) as $issetIndex) {
$this->fixIsset($tokens, $issetIndex);
}
}
/**
* @param int $index of `T_ISSET` token
*/
private function fixIsset(Tokens $tokens, int $index): void
{
$prevTokenIndex = $tokens->getPrevMeaningfulToken($index);
if ($this->isHigherPrecedenceAssociativityOperator($tokens[$prevTokenIndex])) {
return;
}
$startBraceIndex = $tokens->getNextTokenOfKind($index, ['(']);
$endBraceIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $startBraceIndex);
$ternaryQuestionMarkIndex = $tokens->getNextMeaningfulToken($endBraceIndex);
if (!$tokens[$ternaryQuestionMarkIndex]->equals('?')) {
return; // we are not in a ternary operator
}
// search what is inside the isset()
$issetTokens = $this->getMeaningfulSequence($tokens, $startBraceIndex, $endBraceIndex);
if ($this->hasChangingContent($issetTokens)) {
return; // some weird stuff inside the isset
}
$issetCode = $issetTokens->generateCode();
if ('$this' === $issetCode) {
return; // null coalescing operator does not with $this
}
// search what is inside the middle argument of ternary operator
$ternaryColonIndex = $tokens->getNextTokenOfKind($ternaryQuestionMarkIndex, [':']);
$ternaryFirstOperandTokens = $this->getMeaningfulSequence($tokens, $ternaryQuestionMarkIndex, $ternaryColonIndex);
if ($issetCode !== $ternaryFirstOperandTokens->generateCode()) {
return; // regardless of non-meaningful tokens, the operands are different
}
$ternaryFirstOperandIndex = $tokens->getNextMeaningfulToken($ternaryQuestionMarkIndex);
// preserve comments and spaces
$comments = [];
$commentStarted = false;
for ($loopIndex = $index; $loopIndex < $ternaryFirstOperandIndex; ++$loopIndex) {
if ($tokens[$loopIndex]->isComment()) {
$comments[] = $tokens[$loopIndex];
$commentStarted = true;
} elseif ($commentStarted) {
if ($tokens[$loopIndex]->isWhitespace()) {
$comments[] = $tokens[$loopIndex];
}
$commentStarted = false;
}
}
$tokens[$ternaryColonIndex] = new Token([\T_COALESCE, '??']);
$tokens->overrideRange($index, $ternaryFirstOperandIndex - 1, $comments);
}
/**
* Get the sequence of meaningful tokens and returns a new Tokens instance.
*
* @param int $start start index
* @param int $end end index
*/
private function getMeaningfulSequence(Tokens $tokens, int $start, int $end): Tokens
{
$sequence = [];
$index = $start;
while ($index < $end) {
$index = $tokens->getNextMeaningfulToken($index);
if ($index >= $end || null === $index) {
break;
}
$sequence[] = $tokens[$index];
}
return Tokens::fromArray($sequence);
}
/**
* Check if the requested token is an operator computed
* before the ternary operator along with the `isset()`.
*/
private function isHigherPrecedenceAssociativityOperator(Token $token): bool
{
return
$token->isGivenKind([
\T_ARRAY_CAST,
\T_BOOLEAN_AND,
\T_BOOLEAN_OR,
\T_BOOL_CAST,
\T_COALESCE,
\T_DEC,
\T_DOUBLE_CAST,
\T_INC,
\T_INT_CAST,
\T_IS_EQUAL,
\T_IS_GREATER_OR_EQUAL,
\T_IS_IDENTICAL,
\T_IS_NOT_EQUAL,
\T_IS_NOT_IDENTICAL,
\T_IS_SMALLER_OR_EQUAL,
\T_OBJECT_CAST,
\T_POW,
\T_SL,
\T_SPACESHIP,
\T_SR,
\T_STRING_CAST,
\T_UNSET_CAST,
])
|| $token->equalsAny([
'!',
'%',
'&',
'*',
'+',
'-',
'/',
':',
'^',
'|',
'~',
'.',
]);
}
/**
* Check if the `isset()` content may change if called multiple times.
*
* @param Tokens $tokens The original token list
*/
private function hasChangingContent(Tokens $tokens): bool
{
foreach ($tokens as $token) {
if ($token->isGivenKind([
\T_DEC,
\T_INC,
\T_YIELD,
\T_YIELD_FROM,
]) || $token->equals('(')) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,118 @@
<?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\Operator;
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\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
/**
* @phpstan-type _AutogeneratedInputConfiguration array{
* only_dec_inc?: bool,
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* only_dec_inc: bool,
* }
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @author Gregor Harlan <gharlan@web.de>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class UnaryOperatorSpacesFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Unary operators should be placed adjacent to their operands.',
[
new CodeSample("<?php\n\$sample ++;\n-- \$sample;\n\$sample = ! ! \$a;\n\$sample = ~ \$c;\nfunction & foo(){}\n"),
new CodeSample(
'<?php
function foo($a, ... $b) { return (-- $a) * ($b ++);}
',
['only_dec_inc' => false]
),
new CodeSample(
'<?php
function foo($a, ... $b) { return (-- $a) * ($b ++);}
',
['only_dec_inc' => true]
),
]
);
}
/**
* {@inheritdoc}
*
* Must run before NotOperatorWithSpaceFixer, NotOperatorWithSuccessorSpaceFixer.
*/
public function getPriority(): int
{
return 0;
}
public function isCandidate(Tokens $tokens): bool
{
return true;
}
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('only_dec_inc', 'Limit to increment and decrement operators.'))
->setAllowedTypes(['bool'])
->setDefault(false)
->getOption(),
]);
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$tokensAnalyzer = new TokensAnalyzer($tokens);
for ($index = $tokens->count() - 1; $index >= 0; --$index) {
if (true === $this->configuration['only_dec_inc'] && !$tokens[$index]->isGivenKind([\T_DEC, \T_INC])) {
continue;
}
if ($tokensAnalyzer->isUnarySuccessorOperator($index)) {
if (!$tokens[$tokens->getPrevNonWhitespace($index)]->isComment()) {
$tokens->removeLeadingWhitespace($index);
}
continue;
}
if ($tokensAnalyzer->isUnaryPredecessorOperator($index)) {
$tokens->removeTrailingWhitespace($index);
continue;
}
}
}
}