Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
peripherad
/
back
/
vendor
/
friendsofphp
/
php-cs-fixer
/
src
/
Fixer
/
Operator
:
BinaryOperatorSpacesFixer.php
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
<?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> * * @no-named-arguments Parameter names are not covered by the backward compatibility promise. */ 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'; /** * Placeholder used as anchor for right alignment. * * @internal */ public const ALIGN_PLACEHOLDER = "\x2 ALIGNABLE%d \x3"; /** * @var non-empty-list<string> */ private const SUPPORTED_OPERATORS = [ '=', '*', '/', '%', '<', '>', '|', '^', '+', '-', '&', '&=', '&&', '||', '.=', '/=', '=>', '==', '>=', '===', '!=', '<>', '!==', '<=', 'and', 'or', 'xor', '-=', '%=', '*=', '|=', '+=', '<<', '<<=', '>>', '>>=', '^=', '**', '**=', '<=>', '??', '??=', ]; /** * @var non-empty-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' <?php $a= 1 + $b^ $d !== $e or $f; PHP, ), new CodeSample( <<<'PHP' <?php $aa= 1; $b=2; $c = $d xor $e; $f -= 1; PHP, ['operators' => ['=' => self::ALIGN, 'xor' => null]], ), new CodeSample( <<<'PHP' <?php $a = $b +=$c; $d = $ee+=$f; $g = $b +=$c; $h = $ee+=$f; PHP, ['operators' => ['+=' => self::ALIGN_SINGLE_SPACE]], ), new CodeSample( <<<'PHP' <?php $a = $b===$c; $d = $f === $g; $h = $i=== $j; PHP, ['operators' => ['===' => self::ALIGN_SINGLE_SPACE_MINIMAL]], ), new CodeSample( <<<'PHP' <?php $foo = \json_encode($bar, JSON_PRESERVE_ZERO_FRACTION | JSON_PRETTY_PRINT); PHP, ['operators' => ['|' => self::NO_SPACE]], ), new CodeSample( <<<'PHP' <?php $array = [ "foo" => 1, "baaaaaaaaaaar" => 11, ]; PHP, ['operators' => ['=>' => self::SINGLE_SPACE]], ), new CodeSample( <<<'PHP' <?php $array = [ "foo" => 12, "baaaaaaaaaaar" => 13, "baz" => 1, ]; PHP, ['operators' => ['=>' => self::ALIGN]], ), new CodeSample( <<<'PHP' <?php $array = [ "foo" => 12, "baaaaaaaaaaar" => 13, "baz" => 1, ]; PHP, ['operators' => ['=>' => self::ALIGN_BY_SCOPE]], ), new CodeSample( <<<'PHP' <?php $array = [ "foo" => 12, "baaaaaaaaaaar" => 13, "baz" => 1, ]; PHP, ['operators' => ['=>' => self::ALIGN_SINGLE_SPACE]], ), new CodeSample( <<<'PHP' <?php $array = [ "foo" => 12, "baaaaaaaaaaar" => 13, "baz" => 1, ]; PHP, ['operators' => ['=>' => self::ALIGN_SINGLE_SPACE_BY_SCOPE]], ), new CodeSample( <<<'PHP' <?php $array = [ "foo" => 12, "baaaaaaaaaaar" => 13, "baz" => 1, ]; PHP, ['operators' => ['=>' => self::ALIGN_SINGLE_SPACE_MINIMAL]], ), new CodeSample( <<<'PHP' <?php $array = [ "foo" => 12, "baaaaaaaaaaar" => 13, "baz" => 1, ]; PHP, ['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 behaviour 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 analyse 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); } }