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,40 @@
<?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\Tokenizer;
use PhpCsFixer\Utils;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
abstract class AbstractTransformer implements TransformerInterface
{
public function getName(): string
{
$nameParts = explode('\\', static::class);
$name = substr(end($nameParts), 0, -\strlen('Transformer'));
return Utils::camelCaseToUnderscore($name);
}
public function getPriority(): int
{
return 0;
}
abstract public function getCustomTokens(): array;
}

View File

@@ -0,0 +1,89 @@
<?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\Tokenizer;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
abstract class AbstractTypeTransformer extends AbstractTransformer
{
private const TYPE_END_TOKENS = [')', [\T_CALLABLE], [\T_NS_SEPARATOR], [\T_STATIC], [\T_STRING], [CT::T_ARRAY_TYPEHINT]];
private const TYPE_TOKENS = [
'|', '&', '(',
...self::TYPE_END_TOKENS,
[CT::T_TYPE_ALTERNATION], [CT::T_TYPE_INTERSECTION], // some siblings may already be transformed
[\T_WHITESPACE], [\T_COMMENT], [\T_DOC_COMMENT], // technically these can be inside of type tokens array
];
abstract protected function replaceToken(Tokens $tokens, int $index): void;
/**
* @param array{0: int, 1: string}|string $originalToken
*/
protected function doProcess(Tokens $tokens, int $index, $originalToken): void
{
if (!$tokens[$index]->equals($originalToken)) {
return;
}
if (!$this->isPartOfType($tokens, $index)) {
return;
}
$this->replaceToken($tokens, $index);
}
private function isPartOfType(Tokens $tokens, int $index): bool
{
// return types and non-capturing catches
$typeColonIndex = $tokens->getTokenNotOfKindSibling($index, -1, self::TYPE_TOKENS);
if ($tokens[$typeColonIndex]->isGivenKind([\T_CATCH, CT::T_TYPE_COLON, \T_CONST])) {
return true;
}
// for parameter there will be splat operator or variable after the type ("&" is ambiguous and can be reference or bitwise and)
$afterTypeIndex = $tokens->getTokenNotOfKindSibling($index, 1, self::TYPE_TOKENS);
if ($tokens[$afterTypeIndex]->isGivenKind(\T_ELLIPSIS)) {
return true;
}
if (!$tokens[$afterTypeIndex]->isGivenKind(\T_VARIABLE)) {
return false;
}
$beforeVariableIndex = $tokens->getPrevMeaningfulToken($afterTypeIndex);
if ($tokens[$beforeVariableIndex]->equals('&')) {
$prevIndex = $tokens->getPrevTokenOfKind(
$index,
[
'{',
'}',
';',
[\T_CLOSE_TAG],
[\T_FN],
[\T_FUNCTION],
],
);
return null !== $prevIndex && $tokens[$prevIndex]->isGivenKind([\T_FN, \T_FUNCTION]);
}
return $tokens[$beforeVariableIndex]->equalsAny(self::TYPE_END_TOKENS);
}
}

View File

@@ -0,0 +1,121 @@
<?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\Tokenizer\Analyzer;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @internal
*
* @TODO 4.0 remove this analyzer and move this logic into a transformer
*/
final class AlternativeSyntaxAnalyzer
{
private const ALTERNATIVE_SYNTAX_BLOCK_EDGES = [
\T_IF => [\T_ENDIF, \T_ELSE, \T_ELSEIF],
\T_ELSE => [\T_ENDIF],
\T_ELSEIF => [\T_ENDIF, \T_ELSE, \T_ELSEIF],
\T_FOR => [\T_ENDFOR],
\T_FOREACH => [\T_ENDFOREACH],
\T_WHILE => [\T_ENDWHILE],
\T_SWITCH => [\T_ENDSWITCH],
];
public function belongsToAlternativeSyntax(Tokens $tokens, int $index): bool
{
if (!$tokens[$index]->equals(':')) {
return false;
}
$prevIndex = $tokens->getPrevMeaningfulToken($index);
if ($tokens[$prevIndex]->isGivenKind(\T_ELSE)) {
return true;
}
if (!$tokens[$prevIndex]->equals(')')) {
return false;
}
$openParenthesisIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $prevIndex);
$beforeOpenParenthesisIndex = $tokens->getPrevMeaningfulToken($openParenthesisIndex);
return $tokens[$beforeOpenParenthesisIndex]->isGivenKind([
\T_DECLARE,
\T_ELSEIF,
\T_FOR,
\T_FOREACH,
\T_IF,
\T_SWITCH,
\T_WHILE,
]);
}
public function findAlternativeSyntaxBlockEnd(Tokens $tokens, int $index): int
{
if (!isset($tokens[$index])) {
throw new \InvalidArgumentException("There is no token at index {$index}.");
}
if (!$this->isStartOfAlternativeSyntaxBlock($tokens, $index)) {
throw new \InvalidArgumentException("Token at index {$index} is not the start of an alternative syntax block.");
}
$startTokenKind = $tokens[$index]->getId();
if (!isset(self::ALTERNATIVE_SYNTAX_BLOCK_EDGES[$startTokenKind])) {
throw new \LogicException(\sprintf('Unknown startTokenKind: %s', $tokens[$index]->toJson()));
}
$endTokenKinds = self::ALTERNATIVE_SYNTAX_BLOCK_EDGES[$startTokenKind];
$findKinds = [[$startTokenKind]];
foreach ($endTokenKinds as $endTokenKind) {
$findKinds[] = [$endTokenKind];
}
while (true) {
$index = $tokens->getNextTokenOfKind($index, $findKinds);
if ($tokens[$index]->isGivenKind($endTokenKinds)) {
return $index;
}
if ($this->isStartOfAlternativeSyntaxBlock($tokens, $index)) {
$index = $this->findAlternativeSyntaxBlockEnd($tokens, $index);
}
}
}
private function isStartOfAlternativeSyntaxBlock(Tokens $tokens, int $index): bool
{
$map = self::ALTERNATIVE_SYNTAX_BLOCK_EDGES;
$startTokenKind = $tokens[$index]->getId();
if (null === $startTokenKind || !isset($map[$startTokenKind])) {
return false;
}
$index = $tokens->getNextMeaningfulToken($index);
if ($tokens[$index]->equals('(')) {
$index = $tokens->getNextMeaningfulToken(
$tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index)
);
}
return $tokens[$index]->equals(':');
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Tokenizer\Analyzer\Analysis;
/**
* @readonly
*
* @internal
*/
abstract class AbstractControlCaseStructuresAnalysis
{
private int $index;
private int $open;
private int $close;
public function __construct(int $index, int $open, int $close)
{
$this->index = $index;
$this->open = $open;
$this->close = $close;
}
public function getIndex(): int
{
return $this->index;
}
public function getOpenIndex(): int
{
return $this->open;
}
public function getCloseIndex(): int
{
return $this->close;
}
}

View File

@@ -0,0 +1,81 @@
<?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\Tokenizer\Analyzer\Analysis;
/**
* @readonly
*
* @internal
*/
final class ArgumentAnalysis
{
/**
* The name of the argument.
*/
private ?string $name;
/**
* The index where the name is located in the supplied Tokens object.
*/
private ?int $nameIndex;
/**
* The default value of the argument.
*/
private ?string $default;
/**
* The type analysis of the argument.
*/
private ?TypeAnalysis $typeAnalysis;
public function __construct(?string $name, ?int $nameIndex, ?string $default, ?TypeAnalysis $typeAnalysis = null)
{
$this->name = $name;
$this->nameIndex = $nameIndex;
$this->default = $default ?? null;
$this->typeAnalysis = $typeAnalysis ?? null;
}
public function getDefault(): ?string
{
return $this->default;
}
public function hasDefault(): bool
{
return null !== $this->default;
}
public function getName(): ?string
{
return $this->name;
}
public function getNameIndex(): ?int
{
return $this->nameIndex;
}
public function getTypeAnalysis(): ?TypeAnalysis
{
return $this->typeAnalysis;
}
public function hasTypeAnalysis(): bool
{
return null !== $this->typeAnalysis;
}
}

View File

@@ -0,0 +1,76 @@
<?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\Tokenizer\Analyzer\Analysis;
/**
* @readonly
*
* @internal
*
* @phpstan-type _AttributeItem array{start: int, end: int, name: string}
* @phpstan-type _AttributeItems non-empty-list<_AttributeItem>
*/
final class AttributeAnalysis
{
private int $startIndex;
private int $endIndex;
private int $openingBracketIndex;
private int $closingBracketIndex;
/**
* @var _AttributeItems
*/
private array $attributes;
/**
* @param _AttributeItems $attributes
*/
public function __construct(int $startIndex, int $endIndex, int $openingBracketIndex, int $closingBracketIndex, array $attributes)
{
$this->startIndex = $startIndex;
$this->endIndex = $endIndex;
$this->openingBracketIndex = $openingBracketIndex;
$this->closingBracketIndex = $closingBracketIndex;
$this->attributes = $attributes;
}
public function getStartIndex(): int
{
return $this->startIndex;
}
public function getEndIndex(): int
{
return $this->endIndex;
}
public function getOpeningBracketIndex(): int
{
return $this->openingBracketIndex;
}
public function getClosingBracketIndex(): int
{
return $this->closingBracketIndex;
}
/**
* @return _AttributeItems
*/
public function getAttributes(): array
{
return $this->attributes;
}
}

View File

@@ -0,0 +1,45 @@
<?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\Tokenizer\Analyzer\Analysis;
/**
* @author Kuba Werłos <werlos@gmail.com>
*
* @readonly
*
* @internal
*/
final class CaseAnalysis
{
private int $index;
private int $colonIndex;
public function __construct(int $index, int $colonIndex)
{
$this->index = $index;
$this->colonIndex = $colonIndex;
}
public function getIndex(): int
{
return $this->index;
}
public function getColonIndex(): int
{
return $this->colonIndex;
}
}

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\Tokenizer\Analyzer\Analysis;
use PhpCsFixer\Console\Application;
use PhpCsFixer\Utils;
/**
* @internal
*
* @readonly
*/
final class DataProviderAnalysis
{
private string $name;
private int $nameIndex;
/** @var non-empty-list<array{int, int}> */
private array $usageIndices;
/**
* @param non-empty-list<array{int, int}> $usageIndices
*/
public function __construct(string $name, int $nameIndex, array $usageIndices)
{
if ([] === $usageIndices || !array_is_list($usageIndices)) {
Utils::triggerDeprecation(new \InvalidArgumentException(\sprintf(
'Parameter "usageIndices" should be a non-empty-list. This will be enforced in version %d.0.',
Application::getMajorVersion() + 1
)));
}
$this->name = $name;
$this->nameIndex = $nameIndex;
$this->usageIndices = $usageIndices;
}
public function getName(): string
{
return $this->name;
}
public function getNameIndex(): int
{
return $this->nameIndex;
}
/**
* @return non-empty-list<array{int, int}>
*/
public function getUsageIndices(): array
{
return $this->usageIndices;
}
}

View File

@@ -0,0 +1,43 @@
<?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\Tokenizer\Analyzer\Analysis;
/**
* @readonly
*
* @internal
*/
final class DefaultAnalysis
{
private int $index;
private int $colonIndex;
public function __construct(int $index, int $colonIndex)
{
$this->index = $index;
$this->colonIndex = $colonIndex;
}
public function getIndex(): int
{
return $this->index;
}
public function getColonIndex(): int
{
return $this->colonIndex;
}
}

View File

@@ -0,0 +1,46 @@
<?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\Tokenizer\Analyzer\Analysis;
/**
* @readonly
*
* @internal
*/
final class EnumAnalysis extends AbstractControlCaseStructuresAnalysis
{
/**
* @var list<CaseAnalysis>
*/
private array $cases;
/**
* @param list<CaseAnalysis> $cases
*/
public function __construct(int $index, int $open, int $close, array $cases)
{
parent::__construct($index, $open, $close);
$this->cases = $cases;
}
/**
* @return list<CaseAnalysis>
*/
public function getCases(): array
{
return $this->cases;
}
}

View File

@@ -0,0 +1,37 @@
<?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\Tokenizer\Analyzer\Analysis;
/**
* @readonly
*
* @internal
*/
final class MatchAnalysis extends AbstractControlCaseStructuresAnalysis
{
private ?DefaultAnalysis $defaultAnalysis;
public function __construct(int $index, int $open, int $close, ?DefaultAnalysis $defaultAnalysis)
{
parent::__construct($index, $open, $close);
$this->defaultAnalysis = $defaultAnalysis;
}
public function getDefaultAnalysis(): ?DefaultAnalysis
{
return $this->defaultAnalysis;
}
}

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Tokenizer\Analyzer\Analysis;
/**
* @readonly
*
* @internal
*/
final class NamespaceAnalysis
{
/**
* The fully qualified namespace name.
*/
private string $fullName;
/**
* The short version of the namespace.
*/
private string $shortName;
/**
* The start index of the namespace declaration in the analyzed Tokens.
*/
private int $startIndex;
/**
* The end index of the namespace declaration in the analyzed Tokens.
*/
private int $endIndex;
/**
* The start index of the scope of the namespace in the analyzed Tokens.
*/
private int $scopeStartIndex;
/**
* The end index of the scope of the namespace in the analyzed Tokens.
*/
private int $scopeEndIndex;
public function __construct(string $fullName, string $shortName, int $startIndex, int $endIndex, int $scopeStartIndex, int $scopeEndIndex)
{
$this->fullName = $fullName;
$this->shortName = $shortName;
$this->startIndex = $startIndex;
$this->endIndex = $endIndex;
$this->scopeStartIndex = $scopeStartIndex;
$this->scopeEndIndex = $scopeEndIndex;
}
public function getFullName(): string
{
return $this->fullName;
}
public function getShortName(): string
{
return $this->shortName;
}
public function getStartIndex(): int
{
return $this->startIndex;
}
public function getEndIndex(): int
{
return $this->endIndex;
}
public function getScopeStartIndex(): int
{
return $this->scopeStartIndex;
}
public function getScopeEndIndex(): int
{
return $this->scopeEndIndex;
}
public function isGlobalNamespace(): bool
{
return '' === $this->getFullName();
}
}

View File

@@ -0,0 +1,189 @@
<?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\Tokenizer\Analyzer\Analysis;
/**
* @readonly
*
* @internal
*
* @phpstan-type _ImportType 'class'|'constant'|'function'
*
* @author VeeWee <toonverwerft@gmail.com>
* @author Greg Korba <greg@codito.dev>
*/
final class NamespaceUseAnalysis
{
public const TYPE_CLASS = 1; // "classy" could be class, interface or trait
public const TYPE_FUNCTION = 2;
public const TYPE_CONSTANT = 3;
/**
* The fully qualified use namespace.
*
* @var class-string
*/
private string $fullName;
/**
* The short version of use namespace or the alias name in case of aliased use statements.
*/
private string $shortName;
/**
* Is the use statement part of multi-use (`use A, B, C;`, `use A\{B, C};`)?
*/
private bool $isInMulti;
/**
* Is the use statement being aliased?
*/
private bool $isAliased;
/**
* The start index of the namespace declaration in the analyzed Tokens.
*/
private int $startIndex;
/**
* The end index of the namespace declaration in the analyzed Tokens.
*/
private int $endIndex;
/**
* The start index of the single import in the multi-use statement.
*/
private ?int $chunkStartIndex;
/**
* The end index of the single import in the multi-use statement.
*/
private ?int $chunkEndIndex;
/**
* The type of import: class, function or constant.
*
* @var self::TYPE_*
*/
private int $type;
/**
* @param self::TYPE_* $type
* @param class-string $fullName
*/
public function __construct(
int $type,
string $fullName,
string $shortName,
bool $isAliased,
bool $isInMulti,
int $startIndex,
int $endIndex,
?int $chunkStartIndex = null,
?int $chunkEndIndex = null
) {
if (true === $isInMulti && (null === $chunkStartIndex || null === $chunkEndIndex)) {
throw new \LogicException('Chunk start and end index must be set when the import is part of a multi-use statement.');
}
$this->type = $type;
$this->fullName = $fullName;
$this->shortName = $shortName;
$this->isAliased = $isAliased;
$this->isInMulti = $isInMulti;
$this->startIndex = $startIndex;
$this->endIndex = $endIndex;
$this->chunkStartIndex = $chunkStartIndex;
$this->chunkEndIndex = $chunkEndIndex;
}
/**
* @return class-string
*/
public function getFullName(): string
{
return $this->fullName;
}
public function getShortName(): string
{
return $this->shortName;
}
public function isAliased(): bool
{
return $this->isAliased;
}
public function isInMulti(): bool
{
return $this->isInMulti;
}
public function getStartIndex(): int
{
return $this->startIndex;
}
public function getEndIndex(): int
{
return $this->endIndex;
}
public function getChunkStartIndex(): ?int
{
return $this->chunkStartIndex;
}
public function getChunkEndIndex(): ?int
{
return $this->chunkEndIndex;
}
/**
* @return self::TYPE_*
*/
public function getType(): int
{
return $this->type;
}
/**
* @return _ImportType
*/
public function getHumanFriendlyType(): string
{
return [
self::TYPE_CLASS => 'class',
self::TYPE_FUNCTION => 'function',
self::TYPE_CONSTANT => 'constant',
][$this->type];
}
public function isClass(): bool
{
return self::TYPE_CLASS === $this->type;
}
public function isFunction(): bool
{
return self::TYPE_FUNCTION === $this->type;
}
public function isConstant(): bool
{
return self::TYPE_CONSTANT === $this->type;
}
}

View File

@@ -0,0 +1,54 @@
<?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\Tokenizer\Analyzer\Analysis;
/**
* @readonly
*
* @internal
*/
final class SwitchAnalysis extends AbstractControlCaseStructuresAnalysis
{
/**
* @var list<CaseAnalysis>
*/
private array $cases;
private ?DefaultAnalysis $defaultAnalysis;
/**
* @param list<CaseAnalysis> $cases
*/
public function __construct(int $index, int $open, int $close, array $cases, ?DefaultAnalysis $defaultAnalysis)
{
parent::__construct($index, $open, $close);
$this->cases = $cases;
$this->defaultAnalysis = $defaultAnalysis;
}
/**
* @return list<CaseAnalysis>
*/
public function getCases(): array
{
return $this->cases;
}
public function getDefaultAnalysis(): ?DefaultAnalysis
{
return $this->defaultAnalysis;
}
}

View File

@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Tokenizer\Analyzer\Analysis;
/**
* @readonly
*
* @internal
*/
final class TypeAnalysis
{
/**
* This list contains soft and hard reserved types that can be used or will be used by PHP at some point.
*
* More info:
*
* @var list<string>
*
* @see https://php.net/manual/en/functions.arguments.php#functions.arguments.type-declaration.types
* @see https://php.net/manual/en/reserved.other-reserved-words.php
*/
private const RESERVED_TYPES = [
'array',
'bool',
'callable',
'false',
'float',
'int',
'iterable',
'list',
'mixed',
'never',
'null',
'object',
'parent',
'resource',
'self',
'static',
'string',
'true',
'void',
];
private string $name;
private ?int $startIndex;
private ?int $endIndex;
private bool $nullable;
/**
* @param ($startIndex is null ? null : int) $endIndex
*/
public function __construct(string $name, ?int $startIndex = null, ?int $endIndex = null)
{
if (str_starts_with($name, '?')) {
$this->name = substr($name, 1);
$this->nullable = true;
} elseif (\PHP_VERSION_ID >= 8_00_00) {
$this->name = $name;
$this->nullable = \in_array('null', array_map('trim', explode('|', strtolower($name))), true);
} else {
$this->name = $name;
$this->nullable = false;
}
$this->startIndex = $startIndex;
$this->endIndex = $endIndex;
}
public function getName(): string
{
return $this->name;
}
public function getStartIndex(): int
{
if (null === $this->startIndex) {
throw new \RuntimeException('TypeAnalysis: no start index.');
}
return $this->startIndex;
}
public function getEndIndex(): int
{
if (null === $this->endIndex) {
throw new \RuntimeException('TypeAnalysis: no end index.');
}
return $this->endIndex;
}
public function isReservedType(): bool
{
return \in_array(strtolower($this->name), self::RESERVED_TYPES, true);
}
public function isNullable(): bool
{
return $this->nullable;
}
}

View File

@@ -0,0 +1,154 @@
<?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\Tokenizer\Analyzer;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\ArgumentAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\TypeAnalysis;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
* @author Vladimir Reznichenko <kalessil@gmail.com>
*
* @internal
*/
final class ArgumentsAnalyzer
{
private const ARGUMENT_INFO_SKIP_TYPES = [\T_ELLIPSIS, \T_FINAL, CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC, CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED, CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE, FCT::T_READONLY, FCT::T_PRIVATE_SET, FCT::T_PROTECTED_SET, FCT::T_PUBLIC_SET];
/**
* Count amount of parameters in a function/method reference.
*/
public function countArguments(Tokens $tokens, int $openParenthesis, int $closeParenthesis): int
{
return \count($this->getArguments($tokens, $openParenthesis, $closeParenthesis));
}
/**
* Returns start and end token indices of arguments.
*
* Returns an array with each key being the first token of an
* argument and the value the last. Including non-function tokens
* such as comments and white space tokens, but without the separation
* tokens like '(', ',' and ')'.
*
* @return array<int, int>
*/
public function getArguments(Tokens $tokens, int $openParenthesis, int $closeParenthesis): array
{
$arguments = [];
$firstSensibleToken = $tokens->getNextMeaningfulToken($openParenthesis);
if ($tokens[$firstSensibleToken]->equals(')')) {
return $arguments;
}
$paramContentIndex = $openParenthesis + 1;
$argumentsStart = $paramContentIndex;
for (; $paramContentIndex < $closeParenthesis; ++$paramContentIndex) {
$token = $tokens[$paramContentIndex];
// skip nested (), [], {} constructs
$blockDefinitionProbe = Tokens::detectBlockType($token);
if (null !== $blockDefinitionProbe && true === $blockDefinitionProbe['isStart']) {
$paramContentIndex = $tokens->findBlockEnd($blockDefinitionProbe['type'], $paramContentIndex);
continue;
}
// if comma matched, increase arguments counter
if ($token->equals(',')) {
if ($tokens->getNextMeaningfulToken($paramContentIndex) === $closeParenthesis) {
break; // trailing ',' in function call (PHP 7.3)
}
$arguments[$argumentsStart] = $paramContentIndex - 1;
$argumentsStart = $paramContentIndex + 1;
}
}
$arguments[$argumentsStart] = $paramContentIndex - 1;
return $arguments;
}
public function getArgumentInfo(Tokens $tokens, int $argumentStart, int $argumentEnd): ArgumentAnalysis
{
$info = [
'default' => null,
'name' => null,
'name_index' => null,
'type' => null,
'type_index_start' => null,
'type_index_end' => null,
];
$sawName = false;
for ($index = $argumentStart; $index <= $argumentEnd; ++$index) {
$token = $tokens[$index];
if ($token->isGivenKind(FCT::T_ATTRIBUTE)) {
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ATTRIBUTE, $index);
continue;
}
if (
$token->isComment()
|| $token->isWhitespace()
|| $token->isGivenKind(self::ARGUMENT_INFO_SKIP_TYPES)
|| $token->equals('&')
) {
continue;
}
if ($token->isGivenKind(\T_VARIABLE)) {
$sawName = true;
$info['name_index'] = $index;
$info['name'] = $token->getContent();
continue;
}
if ($token->equals('=')) {
continue;
}
if ($sawName) {
$info['default'] .= $token->getContent();
} else {
$info['type_index_start'] = ($info['type_index_start'] > 0) ? $info['type_index_start'] : $index;
$info['type_index_end'] = $index;
$info['type'] .= $token->getContent();
}
}
if (null === $info['name']) {
$info['type'] = null;
}
return new ArgumentAnalysis(
$info['name'],
$info['name_index'],
$info['default'],
null !== $info['type'] ? new TypeAnalysis($info['type'], $info['type_index_start'], $info['type_index_end']) : null
);
}
}

View File

@@ -0,0 +1,208 @@
<?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\Tokenizer\Analyzer;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\AttributeAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @internal
*
* @phpstan-import-type _AttributeItems from AttributeAnalysis
*/
final class AttributeAnalyzer
{
private const TOKEN_KINDS_NOT_ALLOWED_IN_ATTRIBUTE = [
';',
'{',
[\T_ATTRIBUTE],
[\T_FUNCTION],
[\T_OPEN_TAG],
[\T_OPEN_TAG_WITH_ECHO],
[\T_PRIVATE],
[\T_PROTECTED],
[\T_PUBLIC],
[\T_RETURN],
[\T_VARIABLE],
[CT::T_ATTRIBUTE_CLOSE],
];
/**
* Check if given index is an attribute declaration.
*/
public static function isAttribute(Tokens $tokens, int $index): bool
{
if (
!$tokens[$index]->isGivenKind(\T_STRING) // checked token is not a string
|| !$tokens->isAnyTokenKindsFound([FCT::T_ATTRIBUTE]) // no attributes in the tokens collection
) {
return false;
}
$attributeStartIndex = $tokens->getPrevTokenOfKind($index, self::TOKEN_KINDS_NOT_ALLOWED_IN_ATTRIBUTE);
if (!$tokens[$attributeStartIndex]->isGivenKind(\T_ATTRIBUTE)) {
return false;
}
// now, between attribute start and the attribute candidate index cannot be more "(" than ")"
$count = 0;
for ($i = $attributeStartIndex + 1; $i < $index; ++$i) {
if ($tokens[$i]->equals('(')) {
++$count;
} elseif ($tokens[$i]->equals(')')) {
--$count;
}
}
return 0 === $count;
}
/**
* Find all consecutive elements that start with #[ and end with ] and the attributes inside.
*
* @return list<AttributeAnalysis>
*/
public static function collect(Tokens $tokens, int $index): array
{
if (!$tokens[$index]->isGivenKind(\T_ATTRIBUTE)) {
throw new \InvalidArgumentException('Given index must point to an attribute.');
}
// Rewind to first attribute in group
while ($tokens[$prevIndex = $tokens->getPrevMeaningfulToken($index)]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) {
$index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_ATTRIBUTE, $prevIndex);
}
$elements = [];
$openingIndex = $index;
do {
$elements[] = $element = self::collectOne($tokens, $openingIndex);
$openingIndex = $tokens->getNextMeaningfulToken($element->getEndIndex());
} while ($tokens[$openingIndex]->isGivenKind(\T_ATTRIBUTE));
return $elements;
}
/**
* Find one element that starts with #[ and ends with ] and the attributes inside.
*/
public static function collectOne(Tokens $tokens, int $index): AttributeAnalysis
{
if (!$tokens[$index]->isGivenKind(\T_ATTRIBUTE)) {
throw new \InvalidArgumentException('Given index must point to an attribute.');
}
$startIndex = $index;
$prevIndex = $tokens->getPrevMeaningfulToken($index);
if ($tokens[$tokens->getPrevMeaningfulToken($index)]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) {
// Include comments/PHPDoc if they are present
$startIndex = $tokens->getNextNonWhitespace($prevIndex);
}
$closingIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ATTRIBUTE, $index);
$endIndex = $tokens->getNextNonWhitespace($closingIndex);
return new AttributeAnalysis(
$startIndex,
$endIndex - 1,
$index,
$closingIndex,
self::collectAttributes($tokens, $index, $closingIndex),
);
}
public static function determineAttributeFullyQualifiedName(Tokens $tokens, string $name, int $index): string
{
if ('\\' === $name[0]) {
return $name;
}
if (!$tokens[$index]->isGivenKind([\T_STRING, \T_NS_SEPARATOR])) {
$index = $tokens->getNextTokenOfKind($index, [[\T_STRING], [\T_NS_SEPARATOR]]);
}
[$namespaceAnalysis, $namespaceUseAnalyses] = NamespacesAnalyzer::collectNamespaceAnalysis($tokens, $index);
$namespace = $namespaceAnalysis->getFullName();
$firstTokenOfName = $tokens[$index]->getContent();
$namespaceUseAnalysis = $namespaceUseAnalyses[$firstTokenOfName] ?? false;
if ($namespaceUseAnalysis instanceof NamespaceUseAnalysis) {
$namespace = $namespaceUseAnalysis->getFullName();
if ($name === $firstTokenOfName) {
return $namespace;
}
$name = substr((string) strstr($name, '\\'), 1);
}
return $namespace.'\\'.$name;
}
/**
* @return _AttributeItems
*/
private static function collectAttributes(Tokens $tokens, int $index, int $closingIndex): array
{
$elements = [];
do {
$attributeStartIndex = $index + 1;
$nameStartIndex = $tokens->getNextTokenOfKind($index, [[\T_STRING], [\T_NS_SEPARATOR]]);
$index = $tokens->getNextTokenOfKind($attributeStartIndex, ['(', ',', [CT::T_ATTRIBUTE_CLOSE]]);
$attributeName = $tokens->generatePartialCode($nameStartIndex, $tokens->getPrevMeaningfulToken($index));
// Find closing parentheses, we need to do this in case there's a comma inside the parentheses
if ($tokens[$index]->equals('(')) {
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
$index = $tokens->getNextTokenOfKind($index, [',', [CT::T_ATTRIBUTE_CLOSE]]);
}
$elements[] = [
'start' => $attributeStartIndex,
'end' => $index - 1,
'name' => $attributeName,
];
$nextIndex = $index;
// In case there's a comma right before T_ATTRIBUTE_CLOSE
if ($nextIndex < $closingIndex) {
$nextIndex = $tokens->getNextMeaningfulToken($index);
}
} while ($nextIndex < $closingIndex);
// End last element at newline if it exists and there's no trailing comma
--$index;
while ($tokens[$index]->isWhitespace()) {
if (Preg::match('/\R/', $tokens[$index]->getContent())) {
$lastElementKey = array_key_last($elements);
$elements[$lastElementKey]['end'] = $index - 1;
break;
}
--$index;
}
return $elements;
}
}

View File

@@ -0,0 +1,59 @@
<?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\Tokenizer\Analyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Kuba Werłos <werlos@gmail.com>
*
* @internal
*/
final class BlocksAnalyzer
{
public function isBlock(Tokens $tokens, int $openIndex, int $closeIndex): bool
{
if (!$tokens->offsetExists($openIndex)) {
throw new \InvalidArgumentException(\sprintf('Tokex index %d for potential block opening does not exist.', $openIndex));
}
if (!$tokens->offsetExists($closeIndex)) {
throw new \InvalidArgumentException(\sprintf('Token index %d for potential block closure does not exist.', $closeIndex));
}
$blockType = $this->getBlockType($tokens[$openIndex]);
if (null === $blockType) {
return false;
}
return $closeIndex === $tokens->findBlockEnd($blockType, $openIndex);
}
/**
* @return Tokens::BLOCK_TYPE_*
*/
private function getBlockType(Token $token): ?int
{
foreach (Tokens::getBlockEdgeDefinitions() as $blockType => $definition) {
if ($token->equals($definition['start'])) {
return $blockType;
}
}
return null;
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Tokenizer\Analyzer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @internal
*/
final class ClassyAnalyzer
{
public function isClassyInvocation(Tokens $tokens, int $index): bool
{
$token = $tokens[$index];
if (!$token->isGivenKind(\T_STRING)) {
throw new \LogicException(\sprintf('No T_STRING at given index %d, got "%s".', $index, $tokens[$index]->getName()));
}
if ((new Analysis\TypeAnalysis($token->getContent()))->isReservedType()) {
return false;
}
$next = $tokens->getNextMeaningfulToken($index);
$nextToken = $tokens[$next];
if ($nextToken->isGivenKind(\T_NS_SEPARATOR)) {
return false;
}
if ($nextToken->isGivenKind([\T_DOUBLE_COLON, \T_ELLIPSIS, CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION, \T_VARIABLE])) {
return true;
}
$prev = $tokens->getPrevMeaningfulToken($index);
while ($tokens[$prev]->isGivenKind([CT::T_NAMESPACE_OPERATOR, \T_NS_SEPARATOR, \T_STRING])) {
$prev = $tokens->getPrevMeaningfulToken($prev);
}
$prevToken = $tokens[$prev];
if ($prevToken->isGivenKind([\T_EXTENDS, \T_INSTANCEOF, \T_INSTEADOF, \T_IMPLEMENTS, \T_NEW, CT::T_NULLABLE_TYPE, CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION, CT::T_TYPE_COLON, CT::T_USE_TRAIT])) {
return true;
}
if (\PHP_VERSION_ID >= 8_00_00 && $nextToken->equals(')') && $prevToken->equals('(') && $tokens[$tokens->getPrevMeaningfulToken($prev)]->isGivenKind(\T_CATCH)) {
return true;
}
if (AttributeAnalyzer::isAttribute($tokens, $index)) {
return true;
}
// `Foo & $bar` could be:
// - function reference parameter: function baz(Foo & $bar) {}
// - bit operator: $x = Foo & $bar;
if ($nextToken->equals('&') && $tokens[$tokens->getNextMeaningfulToken($next)]->isGivenKind(\T_VARIABLE)) {
$checkIndex = $tokens->getPrevTokenOfKind($prev + 1, [';', '{', '}', [\T_FUNCTION], [\T_OPEN_TAG], [\T_OPEN_TAG_WITH_ECHO]]);
return $tokens[$checkIndex]->isGivenKind(\T_FUNCTION);
}
if (!$prevToken->equals(',')) {
return false;
}
do {
$prev = $tokens->getPrevMeaningfulToken($prev);
} while ($tokens[$prev]->equalsAny([',', [\T_NS_SEPARATOR], [\T_STRING], [CT::T_NAMESPACE_OPERATOR]]));
return $tokens[$prev]->isGivenKind([\T_IMPLEMENTS, CT::T_USE_TRAIT]);
}
}

View File

@@ -0,0 +1,357 @@
<?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\Tokenizer\Analyzer;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Kuba Werłos <werlos@gmail.com>
*
* @internal
*/
final class CommentsAnalyzer
{
private const TYPE_HASH = 1;
private const TYPE_DOUBLE_SLASH = 2;
private const TYPE_SLASH_ASTERISK = 3;
private const SKIP_TYPES = [
\T_PRIVATE,
\T_PROTECTED,
\T_PUBLIC,
\T_VAR,
\T_FUNCTION,
\T_FN,
\T_ABSTRACT,
\T_CONST,
\T_NAMESPACE,
\T_REQUIRE,
\T_REQUIRE_ONCE,
\T_INCLUDE,
\T_INCLUDE_ONCE,
\T_FINAL,
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC,
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED,
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE,
FCT::T_READONLY,
FCT::T_PUBLIC_SET,
FCT::T_PROTECTED_SET,
FCT::T_PRIVATE_SET,
];
public function isHeaderComment(Tokens $tokens, int $index): bool
{
if (!$tokens[$index]->isGivenKind([\T_COMMENT, \T_DOC_COMMENT])) {
throw new \InvalidArgumentException('Given index must point to a comment.');
}
if (null === $tokens->getNextMeaningfulToken($index)) {
return false;
}
$prevIndex = $tokens->getPrevNonWhitespace($index);
if ($tokens[$prevIndex]->equals(';')) {
$braceCloseIndex = $tokens->getPrevMeaningfulToken($prevIndex);
if (!$tokens[$braceCloseIndex]->equals(')')) {
return false;
}
$braceOpenIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $braceCloseIndex);
$declareIndex = $tokens->getPrevMeaningfulToken($braceOpenIndex);
if (!$tokens[$declareIndex]->isGivenKind(\T_DECLARE)) {
return false;
}
$prevIndex = $tokens->getPrevNonWhitespace($declareIndex);
}
return $tokens[$prevIndex]->isGivenKind(\T_OPEN_TAG);
}
/**
* Check if comment at given index precedes structural element.
*
* @see https://github.com/php-fig/fig-standards/blob/master/proposed/phpdoc.md#3-definitions
*/
public function isBeforeStructuralElement(Tokens $tokens, int $index): bool
{
$token = $tokens[$index];
if (!$token->isGivenKind([\T_COMMENT, \T_DOC_COMMENT])) {
throw new \InvalidArgumentException('Given index must point to a comment.');
}
$nextIndex = $this->getNextTokenIndex($tokens, $index);
if (null === $nextIndex || $tokens[$nextIndex]->equals('}')) {
return false;
}
if ($this->isStructuralElement($tokens, $nextIndex)) {
return true;
}
if ($this->isValidControl($tokens, $token, $nextIndex)) {
return true;
}
if ($this->isValidVariable($tokens, $nextIndex)) {
return true;
}
if ($this->isValidVariableAssignment($tokens, $token, $nextIndex)) {
return true;
}
if ($tokens[$nextIndex]->isGivenKind(CT::T_USE_TRAIT)) {
return true;
}
return false;
}
/**
* Check if comment at given index precedes return statement.
*/
public function isBeforeReturn(Tokens $tokens, int $index): bool
{
if (!$tokens[$index]->isGivenKind([\T_COMMENT, \T_DOC_COMMENT])) {
throw new \InvalidArgumentException('Given index must point to a comment.');
}
$nextIndex = $this->getNextTokenIndex($tokens, $index);
if (null === $nextIndex || $tokens[$nextIndex]->equals('}')) {
return false;
}
return $tokens[$nextIndex]->isGivenKind(\T_RETURN);
}
/**
* Return array of indices that are part of a comment started at given index.
*
* @param int $index T_COMMENT index
*
* @return non-empty-list<int>
*/
public function getCommentBlockIndices(Tokens $tokens, int $index): array
{
if (!$tokens[$index]->isGivenKind(\T_COMMENT)) {
throw new \InvalidArgumentException('Given index must point to a comment.');
}
$commentType = $this->getCommentType($tokens[$index]->getContent());
$indices = [$index];
if (self::TYPE_SLASH_ASTERISK === $commentType) {
return $indices;
}
$count = \count($tokens);
++$index;
for (; $index < $count; ++$index) {
if ($tokens[$index]->isComment()) {
if ($commentType === $this->getCommentType($tokens[$index]->getContent())) {
$indices[] = $index;
continue;
}
break;
}
if (!$tokens[$index]->isWhitespace() || $this->getLineBreakCount($tokens, $index, $index + 1) > 1) {
break;
}
}
return $indices;
}
/**
* @see https://github.com/phpDocumentor/fig-standards/blob/master/proposed/phpdoc.md#3-definitions
*/
private function isStructuralElement(Tokens $tokens, int $index): bool
{
$token = $tokens[$index];
if ($token->isClassy() || $token->isGivenKind(self::SKIP_TYPES)) {
return true;
}
if ($token->isGivenKind(\T_CASE)) {
$enumParent = $tokens->getPrevTokenOfKind($index, [[FCT::T_ENUM], [\T_SWITCH]]);
return $tokens[$enumParent]->isGivenKind(FCT::T_ENUM);
}
if ($token->isGivenKind(\T_STATIC)) {
return !$tokens[$tokens->getNextMeaningfulToken($index)]->isGivenKind(\T_DOUBLE_COLON);
}
return false;
}
/**
* Checks control structures (for, foreach, if, switch, while) for correct docblock usage.
*
* @param Token $docsToken docs Token
* @param int $controlIndex index of control structure Token
*/
private function isValidControl(Tokens $tokens, Token $docsToken, int $controlIndex): bool
{
if (!$tokens[$controlIndex]->isGivenKind([
\T_FOR,
\T_FOREACH,
\T_IF,
\T_SWITCH,
\T_WHILE,
])) {
return false;
}
$openParenthesisIndex = $tokens->getNextMeaningfulToken($controlIndex);
$closeParenthesisIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openParenthesisIndex);
$docsContent = $docsToken->getContent();
for ($index = $openParenthesisIndex + 1; $index < $closeParenthesisIndex; ++$index) {
$token = $tokens[$index];
if (
$token->isGivenKind(\T_VARIABLE)
&& str_contains($docsContent, $token->getContent())
) {
return true;
}
}
return false;
}
/**
* Checks variable assignments through `list()`, `print()` etc. calls for correct docblock usage.
*
* @param Token $docsToken docs Token
* @param int $languageConstructIndex index of variable Token
*/
private function isValidVariableAssignment(Tokens $tokens, Token $docsToken, int $languageConstructIndex): bool
{
if (!$tokens[$languageConstructIndex]->isGivenKind([
\T_LIST,
\T_PRINT,
\T_ECHO,
CT::T_DESTRUCTURING_SQUARE_BRACE_OPEN,
])) {
return false;
}
$endKind = $tokens[$languageConstructIndex]->isGivenKind(CT::T_DESTRUCTURING_SQUARE_BRACE_OPEN)
? [CT::T_DESTRUCTURING_SQUARE_BRACE_CLOSE]
: ')';
$endIndex = $tokens->getNextTokenOfKind($languageConstructIndex, [$endKind]);
$docsContent = $docsToken->getContent();
for ($index = $languageConstructIndex + 1; $index < $endIndex; ++$index) {
$token = $tokens[$index];
if ($token->isGivenKind(\T_VARIABLE) && str_contains($docsContent, $token->getContent())) {
return true;
}
}
return false;
}
/**
* Checks variable assignments for correct docblock usage.
*
* @param int $index index of variable Token
*/
private function isValidVariable(Tokens $tokens, int $index): bool
{
if (!$tokens[$index]->isGivenKind(\T_VARIABLE)) {
return false;
}
$nextIndex = $tokens->getNextMeaningfulToken($index);
return $tokens[$nextIndex]->equalsAny([
'=',
// arithmetic assignments
[\T_PLUS_EQUAL, '+='],
[\T_MINUS_EQUAL, '-='],
[\T_MUL_EQUAL, '*='],
[\T_DIV_EQUAL, '/='],
[\T_MOD_EQUAL, '%='],
[\T_POW_EQUAL, '**='],
// bitwise assignments
[\T_AND_EQUAL, '&='],
[\T_OR_EQUAL, '|='],
[\T_XOR_EQUAL, '^='],
[\T_SL_EQUAL, '<<='],
[\T_SR_EQUAL, '>>='],
// other assignments
[\T_COALESCE_EQUAL, '??='],
[\T_CONCAT_EQUAL, '.='],
]);
}
private function getCommentType(string $content): int
{
if (str_starts_with($content, '#')) {
return self::TYPE_HASH;
}
if ('*' === $content[1]) {
return self::TYPE_SLASH_ASTERISK;
}
return self::TYPE_DOUBLE_SLASH;
}
private function getLineBreakCount(Tokens $tokens, int $whiteStart, int $whiteEnd): int
{
$lineCount = 0;
for ($i = $whiteStart; $i < $whiteEnd; ++$i) {
$lineCount += Preg::matchAll('/\R/u', $tokens[$i]->getContent());
}
return $lineCount;
}
private function getNextTokenIndex(Tokens $tokens, int $startIndex): ?int
{
$nextIndex = $startIndex;
do {
$nextIndex = $tokens->getNextMeaningfulToken($nextIndex);
while (null !== $nextIndex && $tokens[$nextIndex]->isGivenKind(FCT::T_ATTRIBUTE)) {
$nextIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ATTRIBUTE, $nextIndex);
$nextIndex = $tokens->getNextMeaningfulToken($nextIndex);
}
} while (null !== $nextIndex && $tokens[$nextIndex]->equals('('));
return $nextIndex;
}
}

View File

@@ -0,0 +1,299 @@
<?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\Tokenizer\Analyzer;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\AbstractControlCaseStructuresAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\CaseAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\DefaultAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\EnumAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\MatchAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\SwitchAnalysis;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Tokens;
final class ControlCaseStructuresAnalyzer
{
private const SUPPORTED_TYPES_WITH_CASE_OR_DEFAULT = [
\T_SWITCH,
FCT::T_MATCH,
FCT::T_ENUM,
];
/**
* @param list<int> $types Token types of interest of which analyzes must be returned
*
* @return \Generator<int, AbstractControlCaseStructuresAnalysis>
*/
public static function findControlStructures(Tokens $tokens, array $types): \Generator
{
if (\count($types) < 1) {
return; // quick skip
}
foreach ($types as $type) {
if (!\in_array($type, self::SUPPORTED_TYPES_WITH_CASE_OR_DEFAULT, true)) {
throw new \InvalidArgumentException(\sprintf('Unexpected type "%d".', $type));
}
}
if (!$tokens->isAnyTokenKindsFound($types)) {
return; // quick skip
}
$depth = -1;
/**
* @var list<array{
* kind: null|int,
* index: int,
* brace_count: int,
* cases: list<array{index: int, open: int}>,
* default: null|array{index: int, open: int},
* alternative_syntax: bool,
* }> $stack
*/
$stack = [];
$isTypeOfInterest = false;
foreach ($tokens as $index => $token) {
if ($token->isGivenKind(self::SUPPORTED_TYPES_WITH_CASE_OR_DEFAULT)) {
++$depth;
$stack[$depth] = [
'kind' => $token->getId(),
'index' => $index,
'brace_count' => 0,
'cases' => [],
'default' => null,
'alternative_syntax' => false,
];
$isTypeOfInterest = \in_array($stack[$depth]['kind'], $types, true);
if ($token->isGivenKind(\T_SWITCH)) {
$index = $tokens->getNextMeaningfulToken($index);
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
$stack[$depth]['open'] = $tokens->getNextMeaningfulToken($index);
$stack[$depth]['alternative_syntax'] = $tokens[$stack[$depth]['open']]->equals(':');
} elseif ($token->isGivenKind(FCT::T_MATCH)) {
$index = $tokens->getNextMeaningfulToken($index);
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
$stack[$depth]['open'] = $tokens->getNextMeaningfulToken($index);
} elseif ($token->isGivenKind(FCT::T_ENUM)) {
$stack[$depth]['open'] = $tokens->getNextTokenOfKind($index, ['{']);
}
continue;
}
if ($depth < 0) {
continue;
}
if ($token->equals('{')) {
++$stack[$depth]['brace_count'];
continue;
}
if ($token->equals('}')) {
--$stack[$depth]['brace_count'];
if (0 === $stack[$depth]['brace_count']) {
if ($stack[$depth]['alternative_syntax']) {
continue;
}
if ($isTypeOfInterest) {
$stack[$depth]['end'] = $index;
yield $stack[$depth]['index'] => self::buildControlCaseStructureAnalysis($stack[$depth]);
}
array_pop($stack);
--$depth;
if ($depth < -1) { // @phpstan-ignore-line
throw new \RuntimeException('Analysis depth count failure.');
}
if (isset($stack[$depth]['kind'])) {
$isTypeOfInterest = \in_array($stack[$depth]['kind'], $types, true);
}
}
continue;
}
if ($tokens[$index]->isGivenKind(\T_ENDSWITCH)) {
if (!$stack[$depth]['alternative_syntax']) {
throw new \RuntimeException('Analysis syntax failure, unexpected "T_ENDSWITCH".');
}
if (\T_SWITCH !== $stack[$depth]['kind']) {
throw new \RuntimeException('Analysis type failure, unexpected "T_ENDSWITCH".');
}
if (0 !== $stack[$depth]['brace_count']) {
throw new \RuntimeException('Analysis count failure, unexpected "T_ENDSWITCH".');
}
$index = $tokens->getNextTokenOfKind($index, [';', [\T_CLOSE_TAG]]);
if ($isTypeOfInterest) {
$stack[$depth]['end'] = $index;
yield $stack[$depth]['index'] => self::buildControlCaseStructureAnalysis($stack[$depth]);
}
array_pop($stack);
--$depth;
if ($depth < -1) { // @phpstan-ignore-line
throw new \RuntimeException('Analysis depth count failure ("T_ENDSWITCH").');
}
if (isset($stack[$depth]['kind'])) {
$isTypeOfInterest = \in_array($stack[$depth]['kind'], $types, true);
}
}
if (!$isTypeOfInterest) {
continue; // don't bother to analyze stuff that caller is not interested in
}
if ($token->isGivenKind(\T_CASE)) {
$stack[$depth]['cases'][] = ['index' => $index, 'open' => self::findCaseOpen($tokens, $stack[$depth]['kind'], $index)];
} elseif ($token->isGivenKind(\T_DEFAULT)) {
if (null !== $stack[$depth]['default']) {
throw new \RuntimeException('Analysis multiple "default" found.');
}
$stack[$depth]['default'] = ['index' => $index, 'open' => self::findDefaultOpen($tokens, $stack[$depth]['kind'], $index)];
}
}
}
/**
* @param array{
* kind: int,
* index: int,
* open: int,
* end: int,
* cases: list<array{index: int, open: int}>,
* default: null|array{index: int, open: int},
* } $analysis
*/
private static function buildControlCaseStructureAnalysis(array $analysis): AbstractControlCaseStructuresAnalysis
{
$default = null === $analysis['default']
? null
: new DefaultAnalysis($analysis['default']['index'], $analysis['default']['open']);
$cases = [];
foreach ($analysis['cases'] as $case) {
$cases[$case['index']] = new CaseAnalysis($case['index'], $case['open']);
}
sort($cases);
if (\T_SWITCH === $analysis['kind']) {
return new SwitchAnalysis(
$analysis['index'],
$analysis['open'],
$analysis['end'],
$cases,
$default
);
}
if (FCT::T_ENUM === $analysis['kind']) {
return new EnumAnalysis(
$analysis['index'],
$analysis['open'],
$analysis['end'],
$cases
);
}
if (FCT::T_MATCH === $analysis['kind']) {
return new MatchAnalysis(
$analysis['index'],
$analysis['open'],
$analysis['end'],
$default
);
}
throw new \InvalidArgumentException(\sprintf('Unexpected type "%d".', $analysis['kind']));
}
private static function findCaseOpen(Tokens $tokens, int $kind, int $index): int
{
if (\T_SWITCH === $kind) {
$ternariesCount = 0;
--$index;
while (true) {
++$index;
if ($tokens[$index]->equalsAny(['(', '{'])) { // skip constructs
$type = Tokens::detectBlockType($tokens[$index]);
$index = $tokens->findBlockEnd($type['type'], $index);
continue;
}
if ($tokens[$index]->equals('?')) {
++$ternariesCount;
continue;
}
if ($tokens[$index]->equalsAny([':', ';'])) {
if (0 === $ternariesCount) {
break;
}
--$ternariesCount;
}
}
return $index;
}
if (FCT::T_ENUM === $kind) {
return $tokens->getNextTokenOfKind($index, ['=', ';']);
}
throw new \InvalidArgumentException(\sprintf('Unexpected case for type "%d".', $kind));
}
private static function findDefaultOpen(Tokens $tokens, int $kind, int $index): int
{
if (\T_SWITCH === $kind) {
return $tokens->getNextTokenOfKind($index, [':', ';']);
}
if (FCT::T_MATCH === $kind) {
return $tokens->getNextTokenOfKind($index, [[\T_DOUBLE_ARROW]]);
}
throw new \InvalidArgumentException(\sprintf('Unexpected default for type "%d".', $kind));
}
}

View File

@@ -0,0 +1,170 @@
<?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\Tokenizer\Analyzer;
use PhpCsFixer\DocBlock\TypeExpression;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\DataProviderAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Kuba Werłos <werlos@gmail.com>
*
* @internal
*
* @phpstan-import-type _AttributeItem from \PhpCsFixer\Tokenizer\Analyzer\Analysis\AttributeAnalysis
*/
final class DataProviderAnalyzer
{
private const REGEX_CLASS = '(?:\\\?+'.TypeExpression::REGEX_IDENTIFIER
.'(\\\\'.TypeExpression::REGEX_IDENTIFIER.')*+)';
/**
* @return list<DataProviderAnalysis>
*/
public function getDataProviders(Tokens $tokens, int $startIndex, int $endIndex): array
{
$fullyQualifiedNameAnalyzer = new FullyQualifiedNameAnalyzer($tokens);
$methods = $this->getMethods($tokens, $startIndex, $endIndex);
$dataProviders = [];
foreach ($methods as $methodIndex) {
[$attributeIndex, $docCommentIndex] = $this->getAttributeIndexAndDocCommentIndices($tokens, $methodIndex);
if (null !== $attributeIndex) {
foreach (AttributeAnalyzer::collect($tokens, $attributeIndex) as $attributeAnalysis) {
foreach ($attributeAnalysis->getAttributes() as $attribute) {
$dataProviderNameIndex = $this->getDataProviderNameIndex($tokens, $fullyQualifiedNameAnalyzer, $attribute);
if (null === $dataProviderNameIndex) {
continue;
}
$dataProviders[substr($tokens[$dataProviderNameIndex]->getContent(), 1, -1)][] = [$dataProviderNameIndex, 0];
}
}
}
if (null !== $docCommentIndex) {
Preg::matchAll(
'/@dataProvider\h+(('.self::REGEX_CLASS.'::)?'.TypeExpression::REGEX_IDENTIFIER.')/',
$tokens[$docCommentIndex]->getContent(),
$matches,
\PREG_OFFSET_CAPTURE
);
foreach ($matches[1] as $k => [$matchName]) {
\assert(isset($matches[0][$k]));
$dataProviders[$matchName][] = [$docCommentIndex, $matches[0][$k][1]];
}
}
}
$dataProviderAnalyses = [];
foreach ($dataProviders as $dataProviderName => $dataProviderUsages) {
$lowercaseDataProviderName = strtolower($dataProviderName);
if (!\array_key_exists($lowercaseDataProviderName, $methods)) {
continue;
}
$dataProviderAnalyses[$methods[$lowercaseDataProviderName]] = new DataProviderAnalysis(
$tokens[$methods[$lowercaseDataProviderName]]->getContent(),
$methods[$lowercaseDataProviderName],
$dataProviderUsages,
);
}
ksort($dataProviderAnalyses);
return array_values($dataProviderAnalyses);
}
/**
* @return array<string, int>
*/
private function getMethods(Tokens $tokens, int $startIndex, int $endIndex): array
{
$functions = [];
for ($index = $startIndex; $index < $endIndex; ++$index) {
if (!$tokens[$index]->isGivenKind(\T_FUNCTION)) {
continue;
}
$functionNameIndex = $tokens->getNextNonWhitespace($index);
if (!$tokens[$functionNameIndex]->isGivenKind(\T_STRING)) {
continue;
}
$functions[strtolower($tokens[$functionNameIndex]->getContent())] = $functionNameIndex;
}
return $functions;
}
/**
* @return array{null|int, null|int}
*/
private function getAttributeIndexAndDocCommentIndices(Tokens $tokens, int $index): array
{
$attributeIndex = null;
$docCommentIndex = null;
while (!$tokens[$index]->equalsAny([';', '{', '}', [\T_OPEN_TAG]])) {
--$index;
if ($tokens[$index]->isGivenKind(FCT::T_ATTRIBUTE)) {
$attributeIndex = $index;
} elseif ($tokens[$index]->isGivenKind(\T_DOC_COMMENT)) {
$docCommentIndex = $index;
}
}
return [$attributeIndex, $docCommentIndex];
}
/**
* @param _AttributeItem $attribute
*/
private function getDataProviderNameIndex(Tokens $tokens, FullyQualifiedNameAnalyzer $fullyQualifiedNameAnalyzer, array $attribute): ?int
{
$fullyQualifiedName = $fullyQualifiedNameAnalyzer->getFullyQualifiedName(
$attribute['name'],
$tokens->getNextMeaningfulToken($attribute['start']),
NamespaceUseAnalysis::TYPE_CLASS,
);
if ('PHPUnit\Framework\Attributes\DataProvider' !== $fullyQualifiedName) {
return null;
}
$closeParenthesisIndex = $tokens->getPrevTokenOfKind($attribute['end'] + 1, [')', [\T_ATTRIBUTE]]);
if ($tokens[$closeParenthesisIndex]->isGivenKind(\T_ATTRIBUTE)) {
return null;
}
$dataProviderNameIndex = $tokens->getPrevMeaningfulToken($closeParenthesisIndex);
if (!$tokens[$dataProviderNameIndex]->isGivenKind(\T_CONSTANT_ENCAPSED_STRING)) {
return null;
}
$openParenthesisIndex = $tokens->getPrevMeaningfulToken($dataProviderNameIndex);
if (!$tokens[$openParenthesisIndex]->equals('(')) {
return null;
}
return $dataProviderNameIndex;
}
}

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\Tokenizer\Analyzer;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @internal
*/
final class FullyQualifiedNameAnalyzer
{
private Tokens $tokens;
/**
* @var list<NamespaceAnalysis>
*/
private array $namespaceAnalyses = [];
/**
* @var array<int, list<NamespaceUseAnalysis>>
*/
private array $namespaceUseAnalyses = [];
public function __construct(Tokens $tokens)
{
$this->tokens = $tokens;
}
/**
* @param NamespaceUseAnalysis::TYPE_* $importType
*/
public function getFullyQualifiedName(string $name, int $indexInNamespace, int $importType): string
{
return ltrim($this->getFullyQualifiedNameWithPossiblyLeadingSlash($name, $indexInNamespace, $importType), '\\');
}
/**
* @param NamespaceUseAnalysis::TYPE_* $importType
*/
private function getFullyQualifiedNameWithPossiblyLeadingSlash(string $name, int $indexInNamespace, int $importType): string
{
if ('\\' === $name[0]) {
return $name;
}
$namespaceAnalysis = $this->getNamespaceAnalysis($indexInNamespace);
$this->namespaceUseAnalyses[$namespaceAnalysis->getStartIndex()] ??= (new NamespaceUsesAnalyzer())->getDeclarationsInNamespace($this->tokens, $namespaceAnalysis);
\assert(isset($this->namespaceUseAnalyses[$namespaceAnalysis->getStartIndex()]));
$declarations = [];
foreach ($this->namespaceUseAnalyses[$namespaceAnalysis->getStartIndex()] as $namespaceUseAnalysis) {
if ($namespaceUseAnalysis->getType() !== $importType) {
continue;
}
$declarations[strtolower($namespaceUseAnalysis->getShortName())] = $namespaceUseAnalysis->getFullName();
}
$lowercaseName = strtolower($name);
foreach ($declarations as $lowercaseShortName => $fullName) {
if ($lowercaseName === $lowercaseShortName) {
return $fullName;
}
if (!str_starts_with($lowercaseName, $lowercaseShortName.'\\')) {
continue;
}
return $fullName.substr($name, \strlen($lowercaseShortName));
}
return $namespaceAnalysis->getFullName().'\\'.$name;
}
private function getNamespaceAnalysis(int $index): NamespaceAnalysis
{
foreach ($this->namespaceAnalyses as $namespace) {
if ($namespace->getScopeStartIndex() <= $index && $namespace->getScopeEndIndex() >= $index) {
return $namespace;
}
}
$namespace = (new NamespacesAnalyzer())->getNamespaceAt($this->tokens, $index);
$this->namespaceAnalyses[] = $namespace;
return $namespace;
}
}

View File

@@ -0,0 +1,276 @@
<?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\Tokenizer\Analyzer;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\ArgumentAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\TypeAnalysis;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @internal
*/
final class FunctionsAnalyzer
{
private const POSSIBLE_KINDS = [
\T_DOUBLE_COLON, \T_FUNCTION, CT::T_NAMESPACE_OPERATOR, \T_NEW, CT::T_RETURN_REF, \T_STRING, \T_OBJECT_OPERATOR, FCT::T_NULLSAFE_OBJECT_OPERATOR, FCT::T_ATTRIBUTE];
/**
* @var array{tokens: string, imports: list<NamespaceUseAnalysis>, declarations: list<int>}
*/
private array $functionsAnalysis = ['tokens' => '', 'imports' => [], 'declarations' => []];
/**
* Important: risky because of the limited (file) scope of the tool.
*/
public function isGlobalFunctionCall(Tokens $tokens, int $index): bool
{
if (!$tokens[$index]->isGivenKind(\T_STRING)) {
return false;
}
$openParenthesisIndex = $tokens->getNextMeaningfulToken($index);
if (!$tokens[$openParenthesisIndex]->equals('(')) {
return false;
}
$previousIsNamespaceSeparator = false;
$prevIndex = $tokens->getPrevMeaningfulToken($index);
if ($tokens[$prevIndex]->isGivenKind(\T_NS_SEPARATOR)) {
$previousIsNamespaceSeparator = true;
$prevIndex = $tokens->getPrevMeaningfulToken($prevIndex);
}
if ($tokens[$prevIndex]->isGivenKind(self::POSSIBLE_KINDS)) {
return false;
}
if ($tokens[$tokens->getNextMeaningfulToken($openParenthesisIndex)]->isGivenKind(CT::T_FIRST_CLASS_CALLABLE)) {
return false;
}
if ($previousIsNamespaceSeparator) {
return true;
}
$functionName = strtolower($tokens[$index]->getContent());
if ('set' === $functionName) {
if (!$tokens[$prevIndex]->equalsAny([[CT::T_PROPERTY_HOOK_BRACE_OPEN], ';', '}'])) {
return true;
}
$closeParenthesisIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openParenthesisIndex);
$afterCloseParenthesisIndex = $tokens->getNextMeaningfulToken($closeParenthesisIndex);
if ($tokens[$afterCloseParenthesisIndex]->equalsAny(['{', [\T_DOUBLE_ARROW]])) {
return false;
}
}
if ($tokens->isChanged() || $tokens->getCodeHash() !== $this->functionsAnalysis['tokens']) {
$this->buildFunctionsAnalysis($tokens);
}
// figure out in which namespace we are
$scopeStartIndex = 0;
$scopeEndIndex = \count($tokens) - 1;
$inGlobalNamespace = false;
foreach ($tokens->getNamespaceDeclarations() as $declaration) {
$scopeStartIndex = $declaration->getScopeStartIndex();
$scopeEndIndex = $declaration->getScopeEndIndex();
if ($index >= $scopeStartIndex && $index <= $scopeEndIndex) {
$inGlobalNamespace = $declaration->isGlobalNamespace();
break;
}
}
// check if the call is to a function declared in the same namespace as the call is done,
// if the call is already in the global namespace than declared functions are in the same
// global namespace and don't need checking
if (!$inGlobalNamespace) {
foreach ($this->functionsAnalysis['declarations'] as $functionNameIndex) {
if ($functionNameIndex < $scopeStartIndex || $functionNameIndex > $scopeEndIndex) {
continue;
}
if (strtolower($tokens[$functionNameIndex]->getContent()) === $functionName) {
return false;
}
}
}
foreach ($this->functionsAnalysis['imports'] as $functionUse) {
if ($functionUse->getStartIndex() < $scopeStartIndex || $functionUse->getEndIndex() > $scopeEndIndex) {
continue;
}
if ($functionName !== strtolower($functionUse->getShortName())) {
continue;
}
// global import like `use function \str_repeat;`
return $functionUse->getShortName() === ltrim($functionUse->getFullName(), '\\');
}
if (AttributeAnalyzer::isAttribute($tokens, $index)) {
return false;
}
return true;
}
/**
* @return array<string, ArgumentAnalysis>
*/
public function getFunctionArguments(Tokens $tokens, int $functionIndex): array
{
$argumentsStart = $tokens->getNextTokenOfKind($functionIndex, ['(']);
$argumentsEnd = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $argumentsStart);
$argumentAnalyzer = new ArgumentsAnalyzer();
$arguments = [];
foreach ($argumentAnalyzer->getArguments($tokens, $argumentsStart, $argumentsEnd) as $start => $end) {
$argumentInfo = $argumentAnalyzer->getArgumentInfo($tokens, $start, $end);
$arguments[$argumentInfo->getName()] = $argumentInfo;
}
return $arguments;
}
public function getFunctionReturnType(Tokens $tokens, int $methodIndex): ?TypeAnalysis
{
$argumentsStart = $tokens->getNextTokenOfKind($methodIndex, ['(']);
$argumentsEnd = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $argumentsStart);
$typeColonIndex = $tokens->getNextMeaningfulToken($argumentsEnd);
if (!$tokens[$typeColonIndex]->isGivenKind(CT::T_TYPE_COLON)) {
return null;
}
$type = '';
$typeStartIndex = $tokens->getNextMeaningfulToken($typeColonIndex);
$typeEndIndex = $typeStartIndex;
$functionBodyStart = $tokens->getNextTokenOfKind($typeColonIndex, ['{', ';', [\T_DOUBLE_ARROW]]);
for ($i = $typeStartIndex; $i < $functionBodyStart; ++$i) {
if ($tokens[$i]->isWhitespace() || $tokens[$i]->isComment()) {
continue;
}
$type .= $tokens[$i]->getContent();
$typeEndIndex = $i;
}
return new TypeAnalysis($type, $typeStartIndex, $typeEndIndex);
}
public function isTheSameClassCall(Tokens $tokens, int $index): bool
{
if (!$tokens->offsetExists($index)) {
throw new \InvalidArgumentException(\sprintf('Token index %d does not exist.', $index));
}
$operatorIndex = $tokens->getPrevMeaningfulToken($index);
if (null === $operatorIndex) {
return false;
}
if (!$tokens[$operatorIndex]->isObjectOperator() && !$tokens[$operatorIndex]->isGivenKind(\T_DOUBLE_COLON)) {
return false;
}
$referenceIndex = $tokens->getPrevMeaningfulToken($operatorIndex);
if (null === $referenceIndex) {
return false;
}
if (!$tokens[$referenceIndex]->equalsAny([[\T_VARIABLE, '$this'], [\T_STRING, 'self'], [\T_STATIC, 'static']], false)) {
return false;
}
return $tokens[$tokens->getNextMeaningfulToken($index)]->equals('(');
}
private function buildFunctionsAnalysis(Tokens $tokens): void
{
$this->functionsAnalysis = [
'tokens' => $tokens->getCodeHash(),
'imports' => [],
'declarations' => [],
];
// find declarations
if ($tokens->isTokenKindFound(\T_FUNCTION)) {
$end = \count($tokens);
for ($i = 0; $i < $end; ++$i) {
// skip classy, we are looking for functions not methods
if ($tokens[$i]->isGivenKind(Token::getClassyTokenKinds())) {
$i = $tokens->getNextTokenOfKind($i, ['(', '{']);
if ($tokens[$i]->equals('(')) { // anonymous class
$i = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $i);
$i = $tokens->getNextTokenOfKind($i, ['{']);
}
$i = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $i);
continue;
}
if (!$tokens[$i]->isGivenKind(\T_FUNCTION)) {
continue;
}
$i = $tokens->getNextMeaningfulToken($i);
if ($tokens[$i]->isGivenKind(CT::T_RETURN_REF)) {
$i = $tokens->getNextMeaningfulToken($i);
}
if (!$tokens[$i]->isGivenKind(\T_STRING)) {
continue;
}
$this->functionsAnalysis['declarations'][] = $i;
}
}
// find imported functions
$namespaceUsesAnalyzer = new NamespaceUsesAnalyzer();
if ($tokens->isTokenKindFound(CT::T_FUNCTION_IMPORT)) {
$declarations = $namespaceUsesAnalyzer->getDeclarationsFromTokens($tokens);
foreach ($declarations as $declaration) {
if ($declaration->isFunction()) {
$this->functionsAnalysis['imports'][] = $declaration;
}
}
}
}
}

View File

@@ -0,0 +1,40 @@
<?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\Tokenizer\Analyzer;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @internal
*/
final class GotoLabelAnalyzer
{
public function belongsToGoToLabel(Tokens $tokens, int $index): bool
{
if (!$tokens[$index]->equals(':')) {
return false;
}
$prevMeaningfulTokenIndex = $tokens->getPrevMeaningfulToken($index);
if (!$tokens[$prevMeaningfulTokenIndex]->isGivenKind(\T_STRING)) {
return false;
}
$prevMeaningfulTokenIndex = $tokens->getPrevMeaningfulToken($prevMeaningfulTokenIndex);
return $tokens[$prevMeaningfulTokenIndex]->equalsAny([':', ';', '{', '}', [\T_OPEN_TAG]]);
}
}

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\Tokenizer\Analyzer;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
/**
* @author VeeWee <toonverwerft@gmail.com>
* @author Greg Korba <greg@codito.dev>
*
* @internal
*
* @TODO Drop `allowMultiUses` opt-in flag when all fixers are updated and can handle multi-use statements.
*/
final class NamespaceUsesAnalyzer
{
/**
* @return list<NamespaceUseAnalysis>
*/
public function getDeclarationsFromTokens(Tokens $tokens, bool $allowMultiUses = false): array
{
$tokenAnalyzer = new TokensAnalyzer($tokens);
$useIndices = $tokenAnalyzer->getImportUseIndexes();
return $this->getDeclarations($tokens, $useIndices, $allowMultiUses);
}
/**
* @return list<NamespaceUseAnalysis>
*/
public function getDeclarationsInNamespace(Tokens $tokens, NamespaceAnalysis $namespace, bool $allowMultiUses = false): array
{
$namespaceUses = [];
foreach ($this->getDeclarationsFromTokens($tokens, $allowMultiUses) as $namespaceUse) {
if ($namespaceUse->getStartIndex() >= $namespace->getScopeStartIndex() && $namespaceUse->getStartIndex() <= $namespace->getScopeEndIndex()) {
$namespaceUses[] = $namespaceUse;
}
}
return $namespaceUses;
}
/**
* @param list<int> $useIndices
*
* @return list<NamespaceUseAnalysis>
*/
private function getDeclarations(Tokens $tokens, array $useIndices, bool $allowMultiUses = false): array
{
$uses = [];
foreach ($useIndices as $index) {
$endIndex = $tokens->getNextTokenOfKind($index, [';', [\T_CLOSE_TAG]]);
$declarations = $this->parseDeclarations($index, $endIndex, $tokens);
if (false === $allowMultiUses) {
$declarations = array_filter($declarations, static fn (NamespaceUseAnalysis $declaration) => !$declaration->isInMulti());
}
if ([] !== $declarations) {
$uses = array_merge($uses, $declarations);
}
}
return $uses;
}
/**
* @return list<NamespaceUseAnalysis>
*/
private function parseDeclarations(int $startIndex, int $endIndex, Tokens $tokens): array
{
$type = $this->determineImportType($tokens, $startIndex);
$potentialMulti = $tokens->getNextTokenOfKind($startIndex, [',', [CT::T_GROUP_IMPORT_BRACE_OPEN]]);
$multi = null !== $potentialMulti && $potentialMulti < $endIndex;
$index = $tokens->getNextTokenOfKind($startIndex, [[\T_STRING], [\T_NS_SEPARATOR]]);
$imports = [];
while (null !== $index && $index <= $endIndex) {
$qualifiedName = $this->getNearestQualifiedName($tokens, $index);
$token = $tokens[$qualifiedName['afterIndex']];
if ($token->isGivenKind(CT::T_GROUP_IMPORT_BRACE_OPEN)) {
$groupStart = $groupIndex = $qualifiedName['afterIndex'];
$groupEnd = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_GROUP_IMPORT_BRACE, $groupStart);
while ($groupIndex < $groupEnd) {
$chunkStart = $tokens->getNextMeaningfulToken($groupIndex);
// Finish parsing on trailing comma (no more chunks there)
if ($tokens[$chunkStart]->isGivenKind(CT::T_GROUP_IMPORT_BRACE_CLOSE)) {
break;
}
$groupQualifiedName = $this->getNearestQualifiedName($tokens, $chunkStart);
$imports[] = new NamespaceUseAnalysis(
$type,
$qualifiedName['fullName'].$groupQualifiedName['fullName'], // @phpstan-ignore argument.type
$groupQualifiedName['shortName'],
$groupQualifiedName['aliased'],
true,
$startIndex,
$endIndex,
$chunkStart,
$tokens->getPrevMeaningfulToken($groupQualifiedName['afterIndex'])
);
$groupIndex = $groupQualifiedName['afterIndex'];
}
$index = $groupIndex;
} elseif ($token->equalsAny([',', ';', [\T_CLOSE_TAG]])) {
$previousToken = $tokens->getPrevMeaningfulToken($qualifiedName['afterIndex']);
if (!$tokens[$previousToken]->isGivenKind(CT::T_GROUP_IMPORT_BRACE_CLOSE)) {
$imports[] = new NamespaceUseAnalysis(
$type,
$qualifiedName['fullName'],
$qualifiedName['shortName'],
$qualifiedName['aliased'],
$multi,
$startIndex,
$endIndex,
$multi ? $index : null,
$multi ? $previousToken : null
);
}
$index = $qualifiedName['afterIndex'];
}
$index = $tokens->getNextMeaningfulToken($index);
}
return $imports;
}
/**
* @return NamespaceUseAnalysis::TYPE_*
*/
private function determineImportType(Tokens $tokens, int $startIndex): int
{
$potentialType = $tokens[$tokens->getNextMeaningfulToken($startIndex)];
if ($potentialType->isGivenKind(CT::T_FUNCTION_IMPORT)) {
return NamespaceUseAnalysis::TYPE_FUNCTION;
}
if ($potentialType->isGivenKind(CT::T_CONST_IMPORT)) {
return NamespaceUseAnalysis::TYPE_CONSTANT;
}
return NamespaceUseAnalysis::TYPE_CLASS;
}
/**
* @return array{fullName: class-string, shortName: string, aliased: bool, afterIndex: int}
*/
private function getNearestQualifiedName(Tokens $tokens, int $index): array
{
$fullName = $shortName = '';
$aliased = false;
while (null !== $index) {
$token = $tokens[$index];
if ($token->isGivenKind(\T_STRING)) {
$shortName = $token->getContent();
if (!$aliased) {
$fullName .= $shortName;
}
} elseif ($token->isGivenKind(\T_NS_SEPARATOR)) {
$fullName .= $token->getContent();
} elseif ($token->isGivenKind(\T_AS)) {
$aliased = true;
} elseif ($token->equalsAny([
',',
';',
[CT::T_GROUP_IMPORT_BRACE_OPEN],
[CT::T_GROUP_IMPORT_BRACE_CLOSE],
[\T_CLOSE_TAG],
])) {
break;
}
$index = $tokens->getNextMeaningfulToken($index);
}
/** @var class-string $fqn */
$fqn = $fullName;
return [
'fullName' => $fqn,
'shortName' => $shortName,
'aliased' => $aliased,
'afterIndex' => $index,
];
}
}

View File

@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Tokenizer\Analyzer;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @internal
*/
final class NamespacesAnalyzer
{
/**
* @return list<NamespaceAnalysis>
*/
public function getDeclarations(Tokens $tokens): array
{
$namespaces = [];
for ($index = 1, $count = \count($tokens); $index < $count; ++$index) {
$token = $tokens[$index];
if (!$token->isGivenKind(\T_NAMESPACE)) {
continue;
}
$declarationEndIndex = $tokens->getNextTokenOfKind($index, [';', '{']);
$namespace = trim($tokens->generatePartialCode($index + 1, $declarationEndIndex - 1));
$declarationParts = explode('\\', $namespace);
$shortName = end($declarationParts);
if ($tokens[$declarationEndIndex]->equals('{')) {
$scopeEndIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $declarationEndIndex);
} else {
$scopeEndIndex = $tokens->getNextTokenOfKind($declarationEndIndex, [[\T_NAMESPACE]]);
if (null === $scopeEndIndex) {
$scopeEndIndex = \count($tokens);
}
--$scopeEndIndex;
}
$namespaces[] = new NamespaceAnalysis(
$namespace,
$shortName,
$index,
$declarationEndIndex,
$index,
$scopeEndIndex
);
// Continue the analysis after the end of this namespace to find the next one
$index = $scopeEndIndex;
}
if (0 === \count($namespaces) && $tokens->isTokenKindFound(\T_OPEN_TAG)) {
$namespaces[] = new NamespaceAnalysis(
'',
'',
$openTagIndex = $tokens[0]->isGivenKind(\T_INLINE_HTML) ? 1 : 0,
$openTagIndex,
$openTagIndex,
\count($tokens) - 1,
);
}
return $namespaces;
}
public function getNamespaceAt(Tokens $tokens, int $index): NamespaceAnalysis
{
if (!$tokens->offsetExists($index)) {
throw new \InvalidArgumentException(\sprintf('Token index %d does not exist.', $index));
}
foreach ($this->getDeclarations($tokens) as $namespace) {
if ($namespace->getScopeStartIndex() <= $index && $namespace->getScopeEndIndex() >= $index) {
return $namespace;
}
}
throw new \LogicException(\sprintf('Unable to get the namespace at index %d.', $index));
}
/**
* @return array{NamespaceAnalysis, array<string, NamespaceUseAnalysis>}
*/
public static function collectNamespaceAnalysis(Tokens $tokens, int $startIndex): array
{
$namespaceAnalysis = (new self())->getNamespaceAt($tokens, $startIndex);
$namespaceUseAnalyses = (new NamespaceUsesAnalyzer())->getDeclarationsInNamespace($tokens, $namespaceAnalysis);
$uses = [];
foreach ($namespaceUseAnalyses as $use) {
if (!$use->isClass()) {
continue;
}
$uses[$use->getShortName()] = $use;
}
return [$namespaceAnalysis, $uses];
}
}

View File

@@ -0,0 +1,81 @@
<?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\Tokenizer\Analyzer;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @internal
*/
final class PhpUnitTestCaseAnalyzer
{
/**
* Returns an indices of PHPUnit classes in reverse appearance order.
* Order is important - it's reverted, so if we inject tokens into collection,
* we do it for bottom of file first, and then to the top of the file, so we
* mitigate risk of not visiting whole collections (final indices).
*
* @return iterable<array{0: int, 1: int}> array of [int start, int end] indices from later to earlier classes
*/
public function findPhpUnitClasses(Tokens $tokens): iterable
{
for ($index = $tokens->count() - 1; $index > 0; --$index) {
if (!$this->isPhpUnitClass($tokens, $index)) {
continue;
}
$startIndex = $tokens->getNextTokenOfKind($index, ['{']);
\assert(\is_int($startIndex));
$endIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $startIndex);
yield [$startIndex, $endIndex];
}
}
private function isPhpUnitClass(Tokens $tokens, int $index): bool
{
if (!$tokens[$index]->isGivenKind(\T_CLASS)) {
return false;
}
$extendsIndex = $tokens->getNextTokenOfKind($index, ['{', [\T_EXTENDS]]);
if (!$tokens[$extendsIndex]->isGivenKind(\T_EXTENDS)) {
return false;
}
if (Preg::match('/(?:Test|TestCase)$/', $tokens[$index]->getContent())) {
return true;
}
while (null !== $index = $tokens->getNextMeaningfulToken($index)) {
if ($tokens[$index]->equals('{')) {
break; // end of class signature
}
if (!$tokens[$index]->isGivenKind(\T_STRING)) {
continue; // not part of extends nor part of implements; so continue
}
if (Preg::match('/(?:Test|TestCase)(?:Interface)?$/', $tokens[$index]->getContent())) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,90 @@
<?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\Tokenizer\Analyzer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @internal
*/
final class RangeAnalyzer
{
private function __construct()
{
// cannot create instance of util. class
}
/**
* Meaningful compare of tokens within ranges.
*
* @param array{start: int, end: int} $range1
* @param array{start: int, end: int} $range2
*/
public static function rangeEqualsRange(Tokens $tokens, array $range1, array $range2): bool
{
$leftStart = $range1['start'];
$leftEnd = $range1['end'];
if ($tokens[$leftStart]->isGivenKind([\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT])) {
$leftStart = $tokens->getNextMeaningfulToken($leftStart);
}
while ($tokens[$leftStart]->equals('(') && $tokens[$leftEnd]->equals(')')) {
$leftStart = $tokens->getNextMeaningfulToken($leftStart);
$leftEnd = $tokens->getPrevMeaningfulToken($leftEnd);
}
$rightStart = $range2['start'];
$rightEnd = $range2['end'];
if ($tokens[$rightStart]->isGivenKind([\T_WHITESPACE, \T_COMMENT, \T_DOC_COMMENT])) {
$rightStart = $tokens->getNextMeaningfulToken($rightStart);
}
while ($tokens[$rightStart]->equals('(') && $tokens[$rightEnd]->equals(')')) {
$rightStart = $tokens->getNextMeaningfulToken($rightStart);
$rightEnd = $tokens->getPrevMeaningfulToken($rightEnd);
}
$arrayOpenTypes = ['[', [CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN]];
$arrayCloseTypes = [']', [CT::T_ARRAY_INDEX_CURLY_BRACE_CLOSE]];
while (true) {
$leftToken = $tokens[$leftStart];
$rightToken = $tokens[$rightStart];
if (
!$leftToken->equals($rightToken)
&& !($leftToken->equalsAny($arrayOpenTypes) && $rightToken->equalsAny($arrayOpenTypes))
&& !($leftToken->equalsAny($arrayCloseTypes) && $rightToken->equalsAny($arrayCloseTypes))
) {
return false;
}
$leftStart = $tokens->getNextMeaningfulToken($leftStart);
$rightStart = $tokens->getNextMeaningfulToken($rightStart);
$reachedLeftEnd = null === $leftStart || $leftStart > $leftEnd; // reached end left or moved over
$reachedRightEnd = null === $rightStart || $rightStart > $rightEnd; // reached end right or moved over
if (!$reachedLeftEnd && !$reachedRightEnd) {
continue;
}
return $reachedLeftEnd && $reachedRightEnd;
}
}
}

View File

@@ -0,0 +1,49 @@
<?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\Tokenizer\Analyzer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Kuba Werłos <werlos@gmail.com>
*
* @internal
*/
final class ReferenceAnalyzer
{
public function isReference(Tokens $tokens, int $index): bool
{
if ($tokens[$index]->isGivenKind(CT::T_RETURN_REF)) {
return true;
}
if (!$tokens[$index]->equals('&')) {
return false;
}
/** @var int $index */
$index = $tokens->getPrevMeaningfulToken($index);
if ($tokens[$index]->equalsAny(['=', [\T_AS], [\T_CALLABLE], [\T_DOUBLE_ARROW], [CT::T_ARRAY_TYPEHINT]])) {
return true;
}
if ($tokens[$index]->isGivenKind(\T_STRING)) {
$index = $tokens->getPrevMeaningfulToken($index);
}
return $tokens[$index]->equalsAny(['(', ',', [\T_NS_SEPARATOR], [CT::T_NULLABLE_TYPE]]);
}
}

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\Tokenizer\Analyzer;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\SwitchAnalysis;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @internal
*/
final class SwitchAnalyzer
{
/** @var array<non-empty-string, list<int>> */
private static array $cache = [];
public static function belongsToSwitch(Tokens $tokens, int $index): bool
{
if (!$tokens[$index]->equals(':')) {
return false;
}
$collectionHash = $tokens->getCollectionHash();
if (!\array_key_exists($collectionHash, self::$cache)) {
self::$cache[$collectionHash] = self::getColonIndicesForSwitch(clone $tokens);
}
$arr = self::$cache[$collectionHash];
return \in_array($index, $arr, true);
}
/**
* @return list<int>
*/
private static function getColonIndicesForSwitch(Tokens $tokens): array
{
$colonIndices = [];
/** @var SwitchAnalysis $analysis */
foreach (ControlCaseStructuresAnalyzer::findControlStructures($tokens, [\T_SWITCH]) as $analysis) {
if ($tokens[$analysis->getOpenIndex()]->equals(':')) {
$colonIndices[] = $analysis->getOpenIndex();
}
foreach ($analysis->getCases() as $case) {
$colonIndices[] = $case->getColonIndex();
}
$defaultAnalysis = $analysis->getDefaultAnalysis();
if (null !== $defaultAnalysis) {
$colonIndices[] = $defaultAnalysis->getColonIndex();
}
}
return $colonIndices;
}
}

View File

@@ -0,0 +1,52 @@
<?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\Tokenizer\Analyzer;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @internal
*/
final class WhitespacesAnalyzer
{
public static function detectIndent(Tokens $tokens, int $index): string
{
while (true) {
$whitespaceIndex = $tokens->getPrevTokenOfKind($index, [[\T_WHITESPACE]]);
if (null === $whitespaceIndex) {
return '';
}
$whitespaceToken = $tokens[$whitespaceIndex];
if (str_contains($whitespaceToken->getContent(), "\n")) {
break;
}
$prevToken = $tokens[$whitespaceIndex - 1];
if ($prevToken->isGivenKind([\T_OPEN_TAG, \T_COMMENT]) && "\n" === substr($prevToken->getContent(), -1)) {
break;
}
$index = $whitespaceIndex;
}
$explodedContent = explode("\n", $whitespaceToken->getContent());
return end($explodedContent);
}
}

View File

@@ -0,0 +1,112 @@
<?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\Tokenizer;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class CT
{
public const T_ARRAY_INDEX_CURLY_BRACE_CLOSE = 10_001;
public const T_ARRAY_INDEX_CURLY_BRACE_OPEN = 10_002;
public const T_ARRAY_SQUARE_BRACE_CLOSE = 10_003;
public const T_ARRAY_SQUARE_BRACE_OPEN = 10_004;
public const T_ARRAY_TYPEHINT = 10_005;
public const T_BRACE_CLASS_INSTANTIATION_CLOSE = 10_006;
public const T_BRACE_CLASS_INSTANTIATION_OPEN = 10_007;
public const T_CLASS_CONSTANT = 10_008;
public const T_CONST_IMPORT = 10_009;
public const T_CURLY_CLOSE = 10_010;
public const T_DESTRUCTURING_SQUARE_BRACE_CLOSE = 10_011;
public const T_DESTRUCTURING_SQUARE_BRACE_OPEN = 10_012;
public const T_DOLLAR_CLOSE_CURLY_BRACES = 10_013;
public const T_DYNAMIC_PROP_BRACE_CLOSE = 10_014;
public const T_DYNAMIC_PROP_BRACE_OPEN = 10_015;
public const T_DYNAMIC_VAR_BRACE_CLOSE = 10_016;
public const T_DYNAMIC_VAR_BRACE_OPEN = 10_017;
public const T_FUNCTION_IMPORT = 10_018;
public const T_GROUP_IMPORT_BRACE_CLOSE = 10_019;
public const T_GROUP_IMPORT_BRACE_OPEN = 10_020;
public const T_NAMESPACE_OPERATOR = 10_021;
public const T_NULLABLE_TYPE = 10_022;
public const T_RETURN_REF = 10_023;
public const T_TYPE_ALTERNATION = 10_024;
public const T_TYPE_COLON = 10_025;
public const T_USE_LAMBDA = 10_026;
public const T_USE_TRAIT = 10_027;
public const T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC = 10_028;
public const T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED = 10_029;
public const T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE = 10_030;
public const T_ATTRIBUTE_CLOSE = 10_031;
public const T_NAMED_ARGUMENT_NAME = 10_032;
public const T_NAMED_ARGUMENT_COLON = 10_033;
public const T_FIRST_CLASS_CALLABLE = 10_034;
public const T_TYPE_INTERSECTION = 10_035;
public const T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_OPEN = 10_036;
public const T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_CLOSE = 10_037;
public const T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN = 10_038;
public const T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE = 10_039;
public const T_PROPERTY_HOOK_BRACE_OPEN = 10_040;
public const T_PROPERTY_HOOK_BRACE_CLOSE = 10_041;
private function __construct() {}
/**
* Get name for custom token.
*
* @param int $value custom token value
*
* @return non-empty-string
*/
public static function getName(int $value): string
{
if (!self::has($value)) {
throw new \InvalidArgumentException(\sprintf('No custom token was found for "%s".', $value));
}
$tokens = self::getMapById();
\assert(isset($tokens[$value]));
return 'CT::'.$tokens[$value];
}
/**
* Check if given custom token exists.
*
* @param int $value custom token value
*/
public static function has(int $value): bool
{
$tokens = self::getMapById();
return isset($tokens[$value]);
}
/**
* @return array<self::T_*, non-empty-string>
*/
private static function getMapById(): array
{
static $constants;
if (null === $constants) {
$reflection = new \ReflectionClass(self::class);
$constants = array_flip($reflection->getConstants());
}
return $constants;
}
}

View File

@@ -0,0 +1,55 @@
<?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\Tokenizer;
/**
* Forward Compatibility Tokens.
*
* Class containing tokens that are not present in the lowest supported PHP version,
* so the code can always use the class constant, instead of checking if the constant is defined
*
* @TODO PHP 8.0+, when mentioned PHP version is required, remove the related consts
* @TODO PHP 8.1+, when mentioned PHP version is required, remove the related consts
* @TODO PHP 8.4+, when mentioned PHP version is required, remove the related consts
* @TODO PHP 8.5+, when mentioned PHP version is required, remove the related consts
*
* @internal
*/
final class FCT
{
// PHP 8.0+
public const T_ATTRIBUTE = \PHP_VERSION_ID >= 8_00_00 ? \T_ATTRIBUTE : -801;
public const T_MATCH = \PHP_VERSION_ID >= 8_00_00 ? \T_MATCH : -802;
public const T_NULLSAFE_OBJECT_OPERATOR = \PHP_VERSION_ID >= 8_00_00 ? \T_NULLSAFE_OBJECT_OPERATOR : -803;
public const T_NAME_FULLY_QUALIFIED = \PHP_VERSION_ID >= 8_00_00 ? \T_NAME_FULLY_QUALIFIED : -804;
public const T_NAME_QUALIFIED = \PHP_VERSION_ID >= 8_00_00 ? \T_NAME_QUALIFIED : -805;
public const T_NAME_RELATIVE = \PHP_VERSION_ID >= 8_00_00 ? \T_NAME_RELATIVE : -806;
// PHP 8.1+
public const T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG = \PHP_VERSION_ID >= 8_01_00 ? \T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG : -811;
public const T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG = \PHP_VERSION_ID >= 8_01_00 ? \T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG : -812;
public const T_ENUM = \PHP_VERSION_ID >= 8_01_00 ? \T_ENUM : -813;
public const T_READONLY = \PHP_VERSION_ID >= 8_01_00 ? \T_READONLY : -814;
// PHP 8.4+
public const T_PRIVATE_SET = \PHP_VERSION_ID >= 8_04_00 ? \T_PRIVATE_SET : -841;
public const T_PROTECTED_SET = \PHP_VERSION_ID >= 8_04_00 ? \T_PROTECTED_SET : -842;
public const T_PUBLIC_SET = \PHP_VERSION_ID >= 8_04_00 ? \T_PUBLIC_SET : -843;
public const T_PROPERTY_C = \PHP_VERSION_ID >= 8_04_00 ? \T_PROPERTY_C : -844;
// PHP 8.5+
public const T_PIPE = \PHP_VERSION_ID >= 8_05_00 ? \T_PIPE : -851;
public const T_VOID_CAST = \PHP_VERSION_ID >= 8_05_00 ? \T_VOID_CAST : -852;
}

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\Tokenizer\Processor;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\WhitespacesFixerConfig;
/**
* @author Greg Korba <greg@codito.dev>
*
* @readonly
*/
final class ImportProcessor
{
private WhitespacesFixerConfig $whitespacesConfig;
public function __construct(WhitespacesFixerConfig $whitespacesConfig)
{
$this->whitespacesConfig = $whitespacesConfig;
}
/**
* @param array{
* const?: array<int|string, class-string>,
* class?: array<int|string, class-string>,
* function?: array<int|string, class-string>
* } $imports
*/
public function insertImports(Tokens $tokens, array $imports, int $atIndex): void
{
$lineEnding = $this->whitespacesConfig->getLineEnding();
if (!$tokens[$atIndex]->isWhitespace() || !str_contains($tokens[$atIndex]->getContent(), "\n")) {
$tokens->insertAt($atIndex, new Token([\T_WHITESPACE, $lineEnding]));
}
foreach ($imports as $type => $typeImports) {
sort($typeImports);
$items = [];
foreach ($typeImports as $name) {
$items = array_merge($items, [
new Token([\T_WHITESPACE, $lineEnding]),
new Token([\T_USE, 'use']),
new Token([\T_WHITESPACE, ' ']),
]);
if ('const' === $type) {
$items[] = new Token([CT::T_CONST_IMPORT, 'const']);
$items[] = new Token([\T_WHITESPACE, ' ']);
} elseif ('function' === $type) {
$items[] = new Token([CT::T_FUNCTION_IMPORT, 'function']);
$items[] = new Token([\T_WHITESPACE, ' ']);
}
$items = array_merge($items, self::tokenizeName($name));
$items[] = new Token(';');
}
$tokens->insertAt($atIndex, $items);
}
}
/**
* @param class-string $name
*
* @return list<Token>
*/
public static function tokenizeName(string $name): array
{
$parts = explode('\\', $name);
$newTokens = [];
if ('' === $parts[0]) {
$newTokens[] = new Token([\T_NS_SEPARATOR, '\\']);
array_shift($parts);
}
foreach ($parts as $part) {
$newTokens[] = new Token([\T_STRING, $part]);
$newTokens[] = new Token([\T_NS_SEPARATOR, '\\']);
}
array_pop($newTokens);
return $newTokens;
}
}

View File

@@ -0,0 +1,519 @@
<?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\Tokenizer;
use PhpCsFixer\Utils;
/**
* Representation of single token.
* As a token prototype you should understand a single element generated by token_get_all.
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @readonly
*/
final class Token
{
/**
* Content of token prototype.
*/
private string $content;
/**
* ID of token prototype, if available.
*/
private ?int $id;
/**
* If token prototype is an array.
*/
private bool $isArray;
/**
* @param array{int, string}|string $token token prototype
*/
public function __construct($token)
{
if (\is_array($token)) {
if (!\is_int($token[0])) {
throw new \InvalidArgumentException(\sprintf(
'Id must be an int, got "%s".',
get_debug_type($token[0])
));
}
if (!\is_string($token[1])) {
throw new \InvalidArgumentException(\sprintf(
'Content must be a string, got "%s".',
get_debug_type($token[1])
));
}
if ('' === $token[1]) {
throw new \InvalidArgumentException('Cannot set empty content for id-based Token.');
}
$this->isArray = true;
$this->id = $token[0];
$this->content = $token[1];
} elseif (\is_string($token)) {
$this->isArray = false;
$this->id = null;
$this->content = $token;
} else {
throw new \InvalidArgumentException(\sprintf('Cannot recognize input value as valid Token prototype, got "%s".', get_debug_type($token)));
}
}
/**
* @return list<int>
*/
public static function getCastTokenKinds(): array
{
return [\T_ARRAY_CAST, \T_BOOL_CAST, \T_DOUBLE_CAST, \T_INT_CAST, \T_OBJECT_CAST, \T_STRING_CAST, \T_UNSET_CAST, FCT::T_VOID_CAST];
}
/**
* Get classy tokens kinds: T_ENUM, T_CLASS, T_INTERFACE and T_TRAIT.
*
* @return list<int>
*/
public static function getClassyTokenKinds(): array
{
return [\T_CLASS, \T_TRAIT, \T_INTERFACE, FCT::T_ENUM];
}
/**
* Get object operator tokens kinds: T_OBJECT_OPERATOR and (if available) T_NULLSAFE_OBJECT_OPERATOR.
*
* @return list<int>
*/
public static function getObjectOperatorKinds(): array
{
return [\T_OBJECT_OPERATOR, FCT::T_NULLSAFE_OBJECT_OPERATOR];
}
/**
* Check if token is equals to given one.
*
* If tokens are arrays, then only keys defined in parameter token are checked.
*
* @param array{0: int, 1?: string}|string|Token $other token or it's prototype
* @param bool $caseSensitive perform a case sensitive comparison
*/
public function equals($other, bool $caseSensitive = true): bool
{
if ('&' === $other) {
return '&' === $this->content && (null === $this->id || $this->isGivenKind([FCT::T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG, FCT::T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG]));
}
if (null === $this->id && '&' === $this->content) {
return $other instanceof self && '&' === $other->content && (null === $other->id || $other->isGivenKind([FCT::T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG, FCT::T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG]));
}
if ($other instanceof self) {
// Inlined getPrototype() on this very hot path.
// We access the private properties of $other directly to save function call overhead.
// This is only possible because $other is of the same class as `self`.
if (!$other->isArray) {
$otherPrototype = $other->content;
} else {
$otherPrototype = [
$other->id,
$other->content,
];
}
} else {
$otherPrototype = $other;
}
if ($this->isArray !== \is_array($otherPrototype)) {
return false;
}
if (!$this->isArray) {
return $this->content === $otherPrototype;
}
if ($this->id !== $otherPrototype[0]) {
return false;
}
if (isset($otherPrototype[1])) {
if ($caseSensitive) {
if ($this->content !== $otherPrototype[1]) {
return false;
}
} elseif (0 !== strcasecmp($this->content, $otherPrototype[1])) {
return false;
}
}
// detect unknown keys
unset($otherPrototype[0], $otherPrototype[1]);
return [] === $otherPrototype;
}
/**
* Check if token is equals to one of given.
*
* @param list<array{0: int, 1?: string}|string|Token> $others array of tokens or token prototypes
* @param bool $caseSensitive perform a case sensitive comparison
*/
public function equalsAny(array $others, bool $caseSensitive = true): bool
{
foreach ($others as $other) {
if ($this->equals($other, $caseSensitive)) {
return true;
}
}
return false;
}
/**
* A helper method used to find out whether a certain input token has to be case-sensitively matched.
*
* @param array<int, bool>|bool $caseSensitive global case sensitiveness or an array of booleans, whose keys should match
* the ones used in $sequence. If any is missing, the default case-sensitive
* comparison is used
* @param int $key the key of the token that has to be looked up
*
* @deprecated
*/
public static function isKeyCaseSensitive($caseSensitive, int $key): bool
{
Utils::triggerDeprecation(new \InvalidArgumentException(\sprintf(
'Method "%s" is deprecated and will be removed in the next major version.',
__METHOD__
)));
if (\is_array($caseSensitive)) {
return $caseSensitive[$key] ?? true;
}
return $caseSensitive;
}
/**
* @return array{int, non-empty-string}|string
*/
public function getPrototype()
{
if (!$this->isArray) {
return $this->content;
}
\assert('' !== $this->content);
return [
$this->id,
$this->content,
];
}
/**
* Get token's content.
*
* It shall be used only for getting the content of token, not for checking it against excepted value.
*/
public function getContent(): string
{
return $this->content;
}
/**
* Get token's id.
*
* It shall be used only for getting the internal id of token, not for checking it against excepted value.
*/
public function getId(): ?int
{
return $this->id;
}
/**
* Get token's name.
*
* It shall be used only for getting the name of token, not for checking it against excepted value.
*
* @return null|non-empty-string token name
*/
public function getName(): ?string
{
if (null === $this->id) {
return null;
}
return self::getNameForId($this->id);
}
/**
* Get token's name.
*
* It shall be used only for getting the name of token, not for checking it against excepted value.
*
* @return null|non-empty-string token name
*/
public static function getNameForId(int $id): ?string
{
if (CT::has($id)) {
return CT::getName($id);
}
$name = token_name($id);
return 'UNKNOWN' === $name ? null : $name;
}
/**
* Generate array containing all keywords that exists in PHP version in use.
*
* @return list<int>
*/
public static function getKeywords(): array
{
static $keywords = null;
if (null === $keywords) {
$keywords = self::getTokenKindsForNames(['T_ABSTRACT', 'T_ARRAY', 'T_AS', 'T_BREAK', 'T_CALLABLE', 'T_CASE',
'T_CATCH', 'T_CLASS', 'T_CLONE', 'T_CONST', 'T_CONTINUE', 'T_DECLARE', 'T_DEFAULT', 'T_DO',
'T_ECHO', 'T_ELSE', 'T_ELSEIF', 'T_EMPTY', 'T_ENDDECLARE', 'T_ENDFOR', 'T_ENDFOREACH',
'T_ENDIF', 'T_ENDSWITCH', 'T_ENDWHILE', 'T_EVAL', 'T_EXIT', 'T_EXTENDS', 'T_FINAL',
'T_FINALLY', 'T_FN', 'T_FOR', 'T_FOREACH', 'T_FUNCTION', 'T_GLOBAL', 'T_GOTO', 'T_HALT_COMPILER',
'T_IF', 'T_IMPLEMENTS', 'T_INCLUDE', 'T_INCLUDE_ONCE', 'T_INSTANCEOF', 'T_INSTEADOF',
'T_INTERFACE', 'T_ISSET', 'T_LIST', 'T_LOGICAL_AND', 'T_LOGICAL_OR', 'T_LOGICAL_XOR',
'T_NAMESPACE', 'T_NEW', 'T_PRINT', 'T_PRIVATE', 'T_PROTECTED', 'T_PUBLIC', 'T_REQUIRE',
'T_REQUIRE_ONCE', 'T_RETURN', 'T_STATIC', 'T_SWITCH', 'T_THROW', 'T_TRAIT', 'T_TRY',
'T_UNSET', 'T_USE', 'T_VAR', 'T_WHILE', 'T_YIELD', 'T_YIELD_FROM',
]) + [
CT::T_ARRAY_TYPEHINT => CT::T_ARRAY_TYPEHINT,
CT::T_CLASS_CONSTANT => CT::T_CLASS_CONSTANT,
CT::T_CONST_IMPORT => CT::T_CONST_IMPORT,
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE => CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE,
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED => CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED,
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC => CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC,
CT::T_FUNCTION_IMPORT => CT::T_FUNCTION_IMPORT,
CT::T_NAMESPACE_OPERATOR => CT::T_NAMESPACE_OPERATOR,
CT::T_USE_LAMBDA => CT::T_USE_LAMBDA,
CT::T_USE_TRAIT => CT::T_USE_TRAIT,
FCT::T_ENUM => FCT::T_ENUM,
FCT::T_MATCH => FCT::T_MATCH,
FCT::T_PRIVATE_SET => FCT::T_PRIVATE_SET,
FCT::T_PROTECTED_SET => FCT::T_PROTECTED_SET,
FCT::T_PUBLIC_SET => FCT::T_PUBLIC_SET,
FCT::T_READONLY => FCT::T_READONLY,
];
}
return $keywords;
}
/**
* Generate array containing all predefined constants that exists in PHP version in use.
*
* @return array<int, int>
*
* @see https://php.net/manual/en/language.constants.predefined.php
*/
public static function getMagicConstants(): array
{
static $magicConstants = null;
if (null === $magicConstants) {
$magicConstants = self::getTokenKindsForNames(['T_CLASS_C', 'T_DIR', 'T_FILE', 'T_FUNC_C', 'T_LINE', 'T_METHOD_C', 'T_NS_C', 'T_TRAIT_C']);
}
return $magicConstants;
}
/**
* Check if token prototype is an array.
*
* @return bool is array
*
* @phpstan-assert-if-true !=null $this->getId()
* @phpstan-assert-if-true !='' $this->getContent()
*/
public function isArray(): bool
{
return $this->isArray;
}
/**
* Check if token is one of type cast tokens.
*
* @phpstan-assert-if-true !='' $this->getContent()
*/
public function isCast(): bool
{
return $this->isGivenKind(self::getCastTokenKinds());
}
/**
* Check if token is one of classy tokens: T_CLASS, T_INTERFACE, T_TRAIT or T_ENUM.
*
* @phpstan-assert-if-true !='' $this->getContent()
*/
public function isClassy(): bool
{
return $this->isGivenKind(self::getClassyTokenKinds());
}
/**
* Check if token is one of comment tokens: T_COMMENT or T_DOC_COMMENT.
*
* @phpstan-assert-if-true !='' $this->getContent()
*/
public function isComment(): bool
{
return $this->isGivenKind([\T_COMMENT, \T_DOC_COMMENT]);
}
/**
* Check if token is one of object operator tokens: T_OBJECT_OPERATOR or T_NULLSAFE_OBJECT_OPERATOR.
*
* @phpstan-assert-if-true !='' $this->getContent()
*/
public function isObjectOperator(): bool
{
return $this->isGivenKind(self::getObjectOperatorKinds());
}
/**
* Check if token is one of given kind.
*
* @param int|list<int> $possibleKind kind or array of kinds
*
* @phpstan-assert-if-true !=null $this->getId()
* @phpstan-assert-if-true !='' $this->getContent()
*/
public function isGivenKind($possibleKind): bool
{
return $this->isArray && (\is_array($possibleKind) ? \in_array($this->id, $possibleKind, true) : $this->id === $possibleKind);
}
/**
* Check if token is a keyword.
*
* @phpstan-assert-if-true !='' $this->getContent()
*/
public function isKeyword(): bool
{
$keywords = self::getKeywords();
return $this->isArray && isset($keywords[$this->id]);
}
/**
* Check if token is a native PHP constant: true, false or null.
*
* @phpstan-assert-if-true !='' $this->getContent()
*/
public function isNativeConstant(): bool
{
return $this->isArray && \in_array(strtolower($this->content), ['true', 'false', 'null'], true);
}
/**
* Returns if the token is of a Magic constants type.
*
* @phpstan-assert-if-true !='' $this->getContent()
*
* @see https://php.net/manual/en/language.constants.predefined.php
*/
public function isMagicConstant(): bool
{
$magicConstants = self::getMagicConstants();
return $this->isArray && isset($magicConstants[$this->id]);
}
/**
* Check if token is whitespace.
*
* @param null|string $whitespaces whitespace characters, default is " \t\n\r\0\x0B"
*/
public function isWhitespace(?string $whitespaces = " \t\n\r\0\x0B"): bool
{
if (null === $whitespaces) {
$whitespaces = " \t\n\r\0\x0B";
}
if ($this->isArray && !$this->isGivenKind(\T_WHITESPACE)) {
return false;
}
return '' === trim($this->content, $whitespaces);
}
/**
* @return array{
* id: null|int,
* name: null|non-empty-string,
* content: string,
* isArray: bool,
* changed: bool,
* }
*/
public function toArray(): array
{
return [
'id' => $this->id,
'name' => $this->getName(),
'content' => $this->content,
'isArray' => $this->isArray,
'changed' => false, // @TODO v4: remove index
];
}
/**
* @return non-empty-string
*/
public function toJson(): string
{
$jsonResult = json_encode($this->toArray(), \JSON_PRETTY_PRINT | \JSON_NUMERIC_CHECK);
if (\JSON_ERROR_NONE !== json_last_error()) {
$jsonResult = json_encode(
[
'errorDescription' => 'Cannot encode Tokens to JSON.',
'rawErrorMessage' => json_last_error_msg(),
],
\JSON_PRETTY_PRINT | \JSON_NUMERIC_CHECK
);
}
\assert(false !== $jsonResult);
return $jsonResult;
}
/**
* @param list<string> $tokenNames
*
* @return array<int, int>
*/
private static function getTokenKindsForNames(array $tokenNames): array
{
$keywords = [];
foreach ($tokenNames as $keywordName) {
$keyword = \constant($keywordName);
$keywords[$keyword] = $keyword;
}
return $keywords;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,867 @@
<?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\Tokenizer;
use PhpCsFixer\Tokenizer\Analyzer\AttributeAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\GotoLabelAnalyzer;
/**
* Analyzer of Tokens collection.
*
* Its role is to provide the ability to analyze collection.
*
* @internal
*
* @phpstan-type _ClassyElementType 'case'|'const'|'method'|'property'|'promoted_property'|'trait_import'
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
* @author Gregor Harlan <gharlan@web.de>
*/
final class TokensAnalyzer
{
/**
* Tokens collection instance.
*/
private Tokens $tokens;
/**
* @readonly
*/
private GotoLabelAnalyzer $gotoLabelAnalyzer;
public function __construct(Tokens $tokens)
{
$this->tokens = $tokens;
$this->gotoLabelAnalyzer = new GotoLabelAnalyzer();
}
/**
* Get indices of methods and properties in classy code (classes, interfaces and traits).
*
* @return array<int, array{classIndex: int, token: Token, type: _ClassyElementType}>
*/
public function getClassyElements(): array
{
$elements = [];
for ($index = 1, $count = \count($this->tokens) - 2; $index < $count; ++$index) {
if ($this->tokens[$index]->isClassy()) {
[$index, $newElements] = $this->findClassyElements($index, $index);
$elements += $newElements;
}
}
ksort($elements);
return $elements;
}
/**
* Get indices of modifiers of a classy code (classes, interfaces and traits).
*
* @return array{
* final: null|int,
* abstract: null|int,
* readonly: null|int
* }
*/
public function getClassyModifiers(int $index): array
{
if (!$this->tokens[$index]->isClassy()) {
throw new \InvalidArgumentException(\sprintf('Not an "classy" at given index %d.', $index));
}
$modifiers = ['final' => null, 'abstract' => null, 'readonly' => null];
while (true) {
$index = $this->tokens->getPrevMeaningfulToken($index);
if ($this->tokens[$index]->isGivenKind(\T_FINAL)) {
$modifiers['final'] = $index;
} elseif ($this->tokens[$index]->isGivenKind(\T_ABSTRACT)) {
$modifiers['abstract'] = $index;
} elseif ($this->tokens[$index]->isGivenKind(FCT::T_READONLY)) {
$modifiers['readonly'] = $index;
} else { // no need to skip attributes as it is not possible on PHP8.2
break;
}
}
return $modifiers;
}
/**
* Get indices of namespace uses.
*
* @param bool $perNamespace Return namespace uses per namespace
*
* @return ($perNamespace is true ? array<int, list<int>> : list<int>)
*/
public function getImportUseIndexes(bool $perNamespace = false): array
{
$tokens = $this->tokens;
$uses = [];
$namespaceIndex = 0;
for ($index = 0, $limit = $tokens->count(); $index < $limit; ++$index) {
$token = $tokens[$index];
if ($token->isGivenKind(\T_NAMESPACE)) {
$nextTokenIndex = $tokens->getNextTokenOfKind($index, [';', '{']);
$nextToken = $tokens[$nextTokenIndex];
if ($nextToken->equals('{')) {
$index = $nextTokenIndex;
}
if ($perNamespace) {
++$namespaceIndex;
}
continue;
}
if ($token->isGivenKind(\T_USE)) {
$uses[$namespaceIndex][] = $index;
}
}
if (!$perNamespace && isset($uses[$namespaceIndex])) {
return $uses[$namespaceIndex];
}
return $uses;
}
/**
* Check if there is an array at given index.
*/
public function isArray(int $index): bool
{
return $this->tokens[$index]->isGivenKind([\T_ARRAY, CT::T_ARRAY_SQUARE_BRACE_OPEN]);
}
/**
* Check if the array at index is multiline.
*
* This only checks the root-level of the array.
*/
public function isArrayMultiLine(int $index): bool
{
if (!$this->isArray($index)) {
throw new \InvalidArgumentException(\sprintf('Not an array at given index %d.', $index));
}
$tokens = $this->tokens;
// Skip only when it's an array, for short arrays we need the brace for correct
// level counting
if ($tokens[$index]->isGivenKind(\T_ARRAY)) {
$index = $tokens->getNextMeaningfulToken($index);
}
return $this->isBlockMultiline($tokens, $index);
}
public function isBlockMultiline(Tokens $tokens, int $index): bool
{
$blockType = Tokens::detectBlockType($tokens[$index]);
if (null === $blockType || !$blockType['isStart']) {
throw new \InvalidArgumentException(\sprintf('Not an block start at given index %d.', $index));
}
$endIndex = $tokens->findBlockEnd($blockType['type'], $index);
for (++$index; $index < $endIndex; ++$index) {
$token = $tokens[$index];
$blockType = Tokens::detectBlockType($token);
if (null !== $blockType && $blockType['isStart']) {
$index = $tokens->findBlockEnd($blockType['type'], $index);
continue;
}
if (
$token->isWhitespace()
&& !$tokens[$index - 1]->isGivenKind(\T_END_HEREDOC)
&& str_contains($token->getContent(), "\n")
) {
return true;
}
}
return false;
}
/**
* @param int $index Index of the T_FUNCTION token
*
* @return array{visibility: null|T_PRIVATE|T_PROTECTED|T_PUBLIC, static: bool, abstract: bool, final: bool}
*/
public function getMethodAttributes(int $index): array
{
if (!$this->tokens[$index]->isGivenKind(\T_FUNCTION)) {
throw new \LogicException(\sprintf('No T_FUNCTION at given index %d, got "%s".', $index, $this->tokens[$index]->getName()));
}
$attributes = [
'visibility' => null,
'static' => false,
'abstract' => false,
'final' => false,
];
for ($i = $index; $i >= 0; --$i) {
$i = $this->tokens->getPrevMeaningfulToken($i);
$token = $this->tokens[$i];
if ($token->isGivenKind(\T_STATIC)) {
$attributes['static'] = true;
continue;
}
if ($token->isGivenKind(\T_FINAL)) {
$attributes['final'] = true;
continue;
}
if ($token->isGivenKind(\T_ABSTRACT)) {
$attributes['abstract'] = true;
continue;
}
// visibility
if ($token->isGivenKind(\T_PRIVATE)) {
$attributes['visibility'] = \T_PRIVATE;
continue;
}
if ($token->isGivenKind(\T_PROTECTED)) {
$attributes['visibility'] = \T_PROTECTED;
continue;
}
if ($token->isGivenKind(\T_PUBLIC)) {
$attributes['visibility'] = \T_PUBLIC;
continue;
}
// found a meaningful token that is not part of
// the function signature; stop looking
break;
}
return $attributes;
}
/**
* Check if there is an anonymous class under given index.
*/
public function isAnonymousClass(int $index): bool
{
if (!$this->tokens[$index]->isClassy()) {
throw new \LogicException(\sprintf('No classy token at given index %d.', $index));
}
if (!$this->tokens[$index]->isGivenKind(\T_CLASS)) {
return false;
}
$index = $this->tokens->getPrevMeaningfulToken($index);
if ($this->tokens[$index]->isGivenKind(FCT::T_READONLY)) {
$index = $this->tokens->getPrevMeaningfulToken($index);
}
while ($this->tokens[$index]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) {
$index = $this->tokens->findBlockStart(Tokens::BLOCK_TYPE_ATTRIBUTE, $index);
$index = $this->tokens->getPrevMeaningfulToken($index);
}
return $this->tokens[$index]->isGivenKind(\T_NEW);
}
/**
* Check if the function under given index is a lambda.
*/
public function isLambda(int $index): bool
{
if (!$this->tokens[$index]->isGivenKind([\T_FUNCTION, \T_FN])) {
throw new \LogicException(\sprintf('No T_FUNCTION or T_FN at given index %d, got "%s".', $index, $this->tokens[$index]->getName()));
}
$startParenthesisIndex = $this->tokens->getNextMeaningfulToken($index);
$startParenthesisToken = $this->tokens[$startParenthesisIndex];
// skip & for `function & () {}` syntax
if ($startParenthesisToken->isGivenKind(CT::T_RETURN_REF)) {
$startParenthesisIndex = $this->tokens->getNextMeaningfulToken($startParenthesisIndex);
$startParenthesisToken = $this->tokens[$startParenthesisIndex];
}
return $startParenthesisToken->equals('(');
}
public function getLastTokenIndexOfArrowFunction(int $index): int
{
if (!$this->tokens[$index]->isGivenKind(\T_FN)) {
throw new \InvalidArgumentException(\sprintf('Not an "arrow function" at given index %d.', $index));
}
$stopTokens = [')', ']', ',', ';', [\T_CLOSE_TAG]];
$index = $this->tokens->getNextTokenOfKind($index, [[\T_DOUBLE_ARROW]]);
while (true) {
$index = $this->tokens->getNextMeaningfulToken($index);
if ($this->tokens[$index]->equalsAny($stopTokens)) {
break;
}
$blockType = Tokens::detectBlockType($this->tokens[$index]);
if (null === $blockType) {
continue;
}
if ($blockType['isStart']) {
$index = $this->tokens->findBlockEnd($blockType['type'], $index);
continue;
}
break;
}
return $this->tokens->getPrevMeaningfulToken($index);
}
/**
* Check if the T_STRING under given index is a constant invocation.
*/
public function isConstantInvocation(int $index): bool
{
if (!$this->tokens[$index]->isGivenKind(\T_STRING)) {
throw new \LogicException(\sprintf('No T_STRING at given index %d, got "%s".', $index, $this->tokens[$index]->getName()));
}
$nextIndex = $this->tokens->getNextMeaningfulToken($index);
if (
$this->tokens[$nextIndex]->equalsAny(['(', '{'])
|| $this->tokens[$nextIndex]->isGivenKind([\T_DOUBLE_COLON, \T_ELLIPSIS, \T_NS_SEPARATOR, CT::T_RETURN_REF, CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION, \T_VARIABLE])
) {
return false;
}
// handle foreach( FOO as $_ ) {}
if ($this->tokens[$nextIndex]->isGivenKind(\T_AS)) {
$prevIndex = $this->tokens->getPrevMeaningfulToken($index);
if (!$this->tokens[$prevIndex]->equals('(')) {
return false;
}
}
$prevIndex = $this->tokens->getPrevMeaningfulToken($index);
if ($this->tokens[$prevIndex]->isGivenKind(Token::getClassyTokenKinds())) {
return false;
}
if ($this->tokens[$prevIndex]->isGivenKind([\T_AS, \T_CONST, \T_DOUBLE_COLON, \T_FUNCTION, \T_GOTO, CT::T_GROUP_IMPORT_BRACE_OPEN, CT::T_TYPE_COLON, CT::T_TYPE_ALTERNATION, CT::T_TYPE_INTERSECTION]) || $this->tokens[$prevIndex]->isObjectOperator()) {
return false;
}
if (
$this->tokens[$prevIndex]->isGivenKind(\T_CASE)
&& $this->tokens->isAllTokenKindsFound([FCT::T_ENUM])
) {
$enumSwitchIndex = $this->tokens->getPrevTokenOfKind($index, [[\T_SWITCH], [\T_ENUM]]);
if (!$this->tokens[$enumSwitchIndex]->isGivenKind(\T_SWITCH)) {
return false;
}
}
while ($this->tokens[$prevIndex]->isGivenKind([CT::T_NAMESPACE_OPERATOR, \T_NS_SEPARATOR, \T_STRING, CT::T_ARRAY_TYPEHINT])) {
$prevIndex = $this->tokens->getPrevMeaningfulToken($prevIndex);
}
if ($this->tokens[$prevIndex]->isGivenKind([CT::T_CONST_IMPORT, \T_EXTENDS, CT::T_FUNCTION_IMPORT, \T_IMPLEMENTS, \T_INSTANCEOF, \T_INSTEADOF, \T_NAMESPACE, \T_NEW, CT::T_NULLABLE_TYPE, CT::T_TYPE_COLON, \T_USE, CT::T_USE_TRAIT, CT::T_TYPE_INTERSECTION, CT::T_TYPE_ALTERNATION, \T_CONST, CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_CLOSE])) {
return false;
}
// `FOO & $bar` could be:
// - function reference parameter: function baz(Foo & $bar) {}
// - bit operator: $x = FOO & $bar;
if ($this->tokens[$nextIndex]->equals('&') && $this->tokens[$this->tokens->getNextMeaningfulToken($nextIndex)]->isGivenKind(\T_VARIABLE)) {
$checkIndex = $this->tokens->getPrevTokenOfKind($prevIndex, [';', '{', '}', [\T_FUNCTION], [\T_OPEN_TAG], [\T_OPEN_TAG_WITH_ECHO]]);
if ($this->tokens[$checkIndex]->isGivenKind(\T_FUNCTION)) {
return false;
}
}
// check for `extends`/`implements`/`use` list
if ($this->tokens[$prevIndex]->equals(',')) {
$checkIndex = $prevIndex;
while ($this->tokens[$checkIndex]->equalsAny([',', [\T_AS], [CT::T_NAMESPACE_OPERATOR], [\T_NS_SEPARATOR], [\T_STRING]])) {
$checkIndex = $this->tokens->getPrevMeaningfulToken($checkIndex);
}
if ($this->tokens[$checkIndex]->isGivenKind([\T_EXTENDS, CT::T_GROUP_IMPORT_BRACE_OPEN, \T_IMPLEMENTS, \T_USE, CT::T_USE_TRAIT])) {
return false;
}
}
// check for array in double quoted string: `"..$foo[bar].."`
if ($this->tokens[$prevIndex]->equals('[') && $this->tokens[$nextIndex]->equals(']')) {
$checkToken = $this->tokens[$this->tokens->getNextMeaningfulToken($nextIndex)];
if ($checkToken->equals('"') || $checkToken->isGivenKind([\T_CURLY_OPEN, \T_DOLLAR_OPEN_CURLY_BRACES, \T_ENCAPSED_AND_WHITESPACE, \T_VARIABLE])) {
return false;
}
}
// check for attribute: `#[Foo]`
if (AttributeAnalyzer::isAttribute($this->tokens, $index)) {
return false;
}
// check for goto label
if ($this->tokens[$nextIndex]->equals(':')) {
if ($this->gotoLabelAnalyzer->belongsToGoToLabel($this->tokens, $nextIndex)) {
return false;
}
}
// check for non-capturing catches
while ($this->tokens[$prevIndex]->isGivenKind([CT::T_NAMESPACE_OPERATOR, \T_NS_SEPARATOR, \T_STRING, CT::T_TYPE_ALTERNATION])) {
$prevIndex = $this->tokens->getPrevMeaningfulToken($prevIndex);
}
if ($this->tokens[$prevIndex]->equals('(')) {
$prevPrevIndex = $this->tokens->getPrevMeaningfulToken($prevIndex);
if ($this->tokens[$prevPrevIndex]->isGivenKind(\T_CATCH)) {
return false;
}
}
return true;
}
/**
* Checks if there is a unary successor operator under given index.
*/
public function isUnarySuccessorOperator(int $index): bool
{
$tokens = $this->tokens;
$token = $tokens[$index];
if (!$token->isGivenKind([\T_INC, \T_DEC])) {
return false;
}
$prevToken = $tokens[$tokens->getPrevMeaningfulToken($index)];
return $prevToken->equalsAny([
']',
[\T_STRING],
[\T_VARIABLE],
[CT::T_ARRAY_INDEX_CURLY_BRACE_CLOSE],
[CT::T_DYNAMIC_PROP_BRACE_CLOSE],
[CT::T_DYNAMIC_VAR_BRACE_CLOSE],
]);
}
/**
* Checks if there is a unary predecessor operator under given index.
*/
public function isUnaryPredecessorOperator(int $index): bool
{
$tokens = $this->tokens;
$token = $tokens[$index];
// potential unary successor operator
if ($token->isGivenKind([\T_INC, \T_DEC])) {
return !$this->isUnarySuccessorOperator($index);
}
// always unary predecessor operator
if ($token->equalsAny(['!', '~', '@', [\T_ELLIPSIS]])) {
return true;
}
// potential binary operator
if (!$token->equalsAny(['+', '-', '&', [CT::T_RETURN_REF]])) {
return false;
}
$prevToken = $tokens[$tokens->getPrevMeaningfulToken($index)];
if (!$prevToken->equalsAny([
']',
'}',
')',
'"',
'`',
[CT::T_ARRAY_SQUARE_BRACE_CLOSE],
[CT::T_ARRAY_INDEX_CURLY_BRACE_CLOSE],
[CT::T_DYNAMIC_PROP_BRACE_CLOSE],
[CT::T_DYNAMIC_VAR_BRACE_CLOSE],
[\T_CLASS_C],
[\T_CONSTANT_ENCAPSED_STRING],
[\T_DEC],
[\T_DIR],
[\T_DNUMBER],
[\T_FILE],
[\T_FUNC_C],
[\T_INC],
[\T_LINE],
[\T_LNUMBER],
[\T_METHOD_C],
[\T_NS_C],
[\T_STRING],
[\T_TRAIT_C],
[\T_VARIABLE],
])) {
return true;
}
if (!$token->equals('&') || !$prevToken->isGivenKind(\T_STRING)) {
return false;
}
$prevToken = $tokens[$tokens->getPrevTokenOfKind($index, [
';',
'{',
'}',
[\T_DOUBLE_ARROW],
[\T_FN],
[\T_FUNCTION],
[\T_OPEN_TAG],
[\T_OPEN_TAG_WITH_ECHO],
])];
return $prevToken->isGivenKind([\T_FN, \T_FUNCTION]);
}
/**
* Checks if there is a binary operator under given index.
*/
public function isBinaryOperator(int $index): bool
{
$tokens = $this->tokens;
$token = $tokens[$index];
if ($token->isGivenKind([\T_INLINE_HTML, \T_ENCAPSED_AND_WHITESPACE, CT::T_TYPE_INTERSECTION])) {
return false;
}
// potential unary predecessor operator
if (\in_array($token->getContent(), ['+', '-', '&'], true)) {
return !$this->isUnaryPredecessorOperator($index);
}
if ($token->isArray()) {
return \in_array($token->getId(), [
\T_AND_EQUAL, // &=
\T_BOOLEAN_AND, // &&
\T_BOOLEAN_OR, // ||
\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_LOGICAL_AND, // and
\T_LOGICAL_OR, // or
\T_LOGICAL_XOR, // xor
\T_MINUS_EQUAL, // -=
\T_MOD_EQUAL, // %=
\T_MUL_EQUAL, // *=
\T_OR_EQUAL, // |=
\T_PLUS_EQUAL, // +=
\T_POW, // **
\T_POW_EQUAL, // **=
\T_SL, // <<
\T_SL_EQUAL, // <<=
\T_SR, // >>
\T_SR_EQUAL, // >>=
\T_XOR_EQUAL, // ^=
\T_SPACESHIP, // <=>
\T_COALESCE, // ??
\T_COALESCE_EQUAL, // ??=
], true);
}
if (\in_array($token->getContent(), ['=', '*', '/', '%', '<', '>', '|', '^', '.'], true)) {
return true;
}
return false;
}
/**
* Check if `T_WHILE` token at given index is `do { ... } while ();` syntax
* and not `while () { ...}`.
*/
public function isWhilePartOfDoWhile(int $index): bool
{
$tokens = $this->tokens;
$token = $tokens[$index];
if (!$token->isGivenKind(\T_WHILE)) {
throw new \LogicException(\sprintf('No T_WHILE at given index %d, got "%s".', $index, $token->getName()));
}
$endIndex = $tokens->getPrevMeaningfulToken($index);
if (!$tokens[$endIndex]->equals('}')) {
return false;
}
$startIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $endIndex);
$beforeStartIndex = $tokens->getPrevMeaningfulToken($startIndex);
return $tokens[$beforeStartIndex]->isGivenKind(\T_DO);
}
/**
* @throws \LogicException when provided index does not point to token containing T_CASE
*/
public function isEnumCase(int $caseIndex): bool
{
$tokens = $this->tokens;
$token = $tokens[$caseIndex];
if (!$token->isGivenKind(\T_CASE)) {
throw new \LogicException(\sprintf(
'No T_CASE given at index %d, got %s instead.',
$caseIndex,
$token->getName() ?? $token->getContent()
));
}
if (!$tokens->isTokenKindFound(FCT::T_ENUM)) {
return false;
}
$prevIndex = $tokens->getPrevTokenOfKind($caseIndex, [[\T_ENUM], [\T_SWITCH]]);
return null !== $prevIndex && $tokens[$prevIndex]->isGivenKind(\T_ENUM);
}
public function isSuperGlobal(int $index): bool
{
$token = $this->tokens[$index];
if (!$token->isGivenKind(\T_VARIABLE)) {
return false;
}
return \in_array(strtoupper($token->getContent()), [
'$_COOKIE',
'$_ENV',
'$_FILES',
'$_GET',
'$_POST',
'$_REQUEST',
'$_SERVER',
'$_SESSION',
'$GLOBALS',
], true);
}
/**
* Find classy elements.
*
* Searches in tokens from the classy (start) index till the end (index) of the classy.
* Returns an array; first value is the index until the method has analysed (int), second the found classy elements (array).
*
* @param int $classIndex classy index
*
* @return array{int, array<int, array{classIndex: int, token: Token, type: _ClassyElementType}>}
*/
private function findClassyElements(int $classIndex, int $index): array
{
$elements = [];
$curlyBracesLevel = 0;
$bracesLevel = 0;
++$index; // skip the classy index itself
for ($count = \count($this->tokens); $index < $count; ++$index) {
$token = $this->tokens[$index];
if ($token->isGivenKind(\T_ENCAPSED_AND_WHITESPACE)) {
continue;
}
if ($token->isGivenKind(\T_CLASS)) { // anonymous class in class
// check for nested anonymous classes inside the new call of an anonymous class,
// for example `new class(function (){new class(function (){new class(function (){}){};}){};}){};` etc.
// if class(XYZ) {} skip till `(` as XYZ might contain functions etc.
$nestedClassIndex = $index;
$index = $this->tokens->getNextMeaningfulToken($index);
if ($this->tokens[$index]->equals('(')) {
++$index; // move after `(`
for ($nestedBracesLevel = 1; $index < $count; ++$index) {
$token = $this->tokens[$index];
if ($token->equals('(')) {
++$nestedBracesLevel;
continue;
}
if ($token->equals(')')) {
--$nestedBracesLevel;
if (0 === $nestedBracesLevel) {
[$index, $newElements] = $this->findClassyElements($nestedClassIndex, $index);
$elements += $newElements;
break;
}
continue;
}
if ($token->isGivenKind(\T_CLASS)) { // anonymous class in class
[$index, $newElements] = $this->findClassyElements($index, $index);
$elements += $newElements;
}
}
} else {
[$index, $newElements] = $this->findClassyElements($nestedClassIndex, $nestedClassIndex);
$elements += $newElements;
}
continue;
}
if ($token->equals('(')) {
++$bracesLevel;
continue;
}
if ($token->equals(')')) {
--$bracesLevel;
continue;
}
if ($token->equals('{')) {
++$curlyBracesLevel;
continue;
}
if ($token->equals('}')) {
--$curlyBracesLevel;
if (0 === $curlyBracesLevel) {
break;
}
continue;
}
if (1 !== $curlyBracesLevel || !$token->isArray()) {
continue;
}
if (0 === $bracesLevel && $token->isGivenKind(\T_VARIABLE)) {
$elements[$index] = [
'classIndex' => $classIndex,
'token' => $token,
'type' => 'property',
];
continue;
}
if ($token->isGivenKind(CT::T_PROPERTY_HOOK_BRACE_OPEN)) {
$index = $this->tokens->getNextTokenOfKind($index, [[CT::T_PROPERTY_HOOK_BRACE_CLOSE]]);
continue;
}
if ($token->isGivenKind(\T_FUNCTION)) {
$elements[$index] = [
'classIndex' => $classIndex,
'token' => $token,
'type' => 'method',
];
$functionNameIndex = $this->tokens->getNextMeaningfulToken($index);
if ('__construct' === $this->tokens[$functionNameIndex]->getContent()) {
$openParenthesis = $this->tokens->getNextMeaningfulToken($functionNameIndex);
$closeParenthesis = $this->tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openParenthesis);
foreach ($this->tokens->findGivenKind([CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC, CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED, CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE, FCT::T_READONLY, \T_FINAL], $openParenthesis, $closeParenthesis) as $kindElements) {
foreach (array_keys($kindElements) as $promotedPropertyModifierIndex) {
/** @var int $promotedPropertyVariableIndex */
$promotedPropertyVariableIndex = $this->tokens->getNextTokenOfKind($promotedPropertyModifierIndex, [[\T_VARIABLE]]);
$elements[$promotedPropertyVariableIndex] = [
'classIndex' => $classIndex,
'token' => $this->tokens[$promotedPropertyVariableIndex],
'type' => 'promoted_property',
];
}
}
}
} elseif ($token->isGivenKind(\T_CONST)) {
$elements[$index] = [
'classIndex' => $classIndex,
'token' => $token,
'type' => 'const',
];
} elseif ($token->isGivenKind(CT::T_USE_TRAIT)) {
$elements[$index] = [
'classIndex' => $classIndex,
'token' => $token,
'type' => 'trait_import',
];
} elseif ($token->isGivenKind(\T_CASE)) {
$elements[$index] = [
'classIndex' => $classIndex,
'token' => $token,
'type' => 'case',
];
}
}
return [$index, $elements];
}
}

View File

@@ -0,0 +1,54 @@
<?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\Tokenizer\Transformer;
use PhpCsFixer\Tokenizer\AbstractTransformer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* Transform `array` typehint from T_ARRAY into CT::T_ARRAY_TYPEHINT.
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class ArrayTypehintTransformer extends AbstractTransformer
{
public function getRequiredPhpVersionId(): int
{
return 5_00_00;
}
public function process(Tokens $tokens, Token $token, int $index): void
{
if (!$token->isGivenKind(\T_ARRAY)) {
return;
}
$nextIndex = $tokens->getNextMeaningfulToken($index);
$nextToken = $tokens[$nextIndex];
if (!$nextToken->equals('(')) {
$tokens[$index] = new Token([CT::T_ARRAY_TYPEHINT, $token->getContent()]);
}
}
public function getCustomTokens(): array
{
return [CT::T_ARRAY_TYPEHINT];
}
}

View File

@@ -0,0 +1,63 @@
<?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\Tokenizer\Transformer;
use PhpCsFixer\Tokenizer\AbstractTransformer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* Transforms attribute related Tokens.
*
* @internal
*/
final class AttributeTransformer extends AbstractTransformer
{
public function getPriority(): int
{
// must run before all other transformers that might touch attributes
return 200;
}
public function getRequiredPhpVersionId(): int
{
return 8_00_00;
}
public function process(Tokens $tokens, Token $token, int $index): void
{
if (!$tokens[$index]->isGivenKind(\T_ATTRIBUTE)) {
return;
}
do {
++$index;
if ($tokens[$index]->equals('(')) {
$index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index) + 1;
}
} while (!$tokens[$index]->equals(']'));
$tokens[$index] = new Token([CT::T_ATTRIBUTE_CLOSE, ']']);
}
public function getCustomTokens(): array
{
return [
CT::T_ATTRIBUTE_CLOSE,
];
}
}

View File

@@ -0,0 +1,80 @@
<?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\Tokenizer\Transformer;
use PhpCsFixer\Tokenizer\AbstractTransformer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* Transform braced class instantiation braces in `(new Foo())` into CT::T_BRACE_CLASS_INSTANTIATION_OPEN
* and CT::T_BRACE_CLASS_INSTANTIATION_CLOSE.
*
* @author Sebastiaans Stok <s.stok@rollerscapes.net>
*
* @internal
*/
final class BraceClassInstantiationTransformer extends AbstractTransformer
{
public function getPriority(): int
{
// must run after CurlyBraceTransformer and SquareBraceTransformer
return -2;
}
public function getRequiredPhpVersionId(): int
{
return 5_00_00;
}
public function process(Tokens $tokens, Token $token, int $index): void
{
if (!$tokens[$index]->equals('(') || !$tokens[$tokens->getNextMeaningfulToken($index)]->isGivenKind(\T_NEW)) {
return;
}
if ($tokens[$tokens->getPrevMeaningfulToken($index)]->equalsAny([
')',
']',
[CT::T_ARRAY_INDEX_CURLY_BRACE_CLOSE],
[CT::T_ARRAY_SQUARE_BRACE_CLOSE],
[CT::T_BRACE_CLASS_INSTANTIATION_CLOSE],
[\T_ARRAY],
[\T_CLASS],
[\T_ELSEIF],
[\T_FOR],
[\T_FOREACH],
[\T_IF],
[\T_STATIC],
[\T_STRING],
[\T_SWITCH],
[\T_VARIABLE],
[\T_WHILE],
])) {
return;
}
$closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
$tokens[$index] = new Token([CT::T_BRACE_CLASS_INSTANTIATION_OPEN, '(']);
$tokens[$closeIndex] = new Token([CT::T_BRACE_CLASS_INSTANTIATION_CLOSE, ')']);
}
public function getCustomTokens(): array
{
return [CT::T_BRACE_CLASS_INSTANTIATION_OPEN, CT::T_BRACE_CLASS_INSTANTIATION_CLOSE];
}
}

View File

@@ -0,0 +1,361 @@
<?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\Tokenizer\Transformer;
use PhpCsFixer\Tokenizer\AbstractTransformer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* Transform discriminate overloaded curly braces tokens.
*
* Performed transformations:
* - closing `}` for T_CURLY_OPEN into CT::T_CURLY_CLOSE,
* - closing `}` for T_DOLLAR_OPEN_CURLY_BRACES into CT::T_DOLLAR_CLOSE_CURLY_BRACES,
* - in `$foo->{$bar}` into CT::T_DYNAMIC_PROP_BRACE_OPEN and CT::T_DYNAMIC_PROP_BRACE_CLOSE,
* - in `${$foo}` into CT::T_DYNAMIC_VAR_BRACE_OPEN and CT::T_DYNAMIC_VAR_BRACE_CLOSE,
* - in `$array{$index}` into CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN and CT::T_ARRAY_INDEX_CURLY_BRACE_CLOSE,
* - in `use some\a\{ClassA, ClassB, ClassC as C}` into CT::T_GROUP_IMPORT_BRACE_OPEN, CT::T_GROUP_IMPORT_BRACE_CLOSE,
* - in `class PropertyHooks { public string $bar _{_ set(string $value) { } _}_` into CT::T_PROPERTY_HOOK_BRACE_OPEN, CT::T_PROPERTY_HOOK_BRACE_CLOSE.
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class BraceTransformer extends AbstractTransformer
{
public function getRequiredPhpVersionId(): int
{
return 5_00_00;
}
public function process(Tokens $tokens, Token $token, int $index): void
{
$this->transformIntoCurlyCloseBrace($tokens, $index);
$this->transformIntoDollarCloseBrace($tokens, $index);
$this->transformIntoDynamicPropBraces($tokens, $index);
$this->transformIntoDynamicVarBraces($tokens, $index);
$this->transformIntoPropertyHookBraces($tokens, $index);
$this->transformIntoCurlyIndexBraces($tokens, $index);
$this->transformIntoGroupUseBraces($tokens, $index);
$this->transformIntoDynamicClassConstantFetchBraces($tokens, $index);
}
public function getCustomTokens(): array
{
return [
CT::T_CURLY_CLOSE,
CT::T_DOLLAR_CLOSE_CURLY_BRACES,
CT::T_DYNAMIC_PROP_BRACE_OPEN,
CT::T_DYNAMIC_PROP_BRACE_CLOSE,
CT::T_DYNAMIC_VAR_BRACE_OPEN,
CT::T_DYNAMIC_VAR_BRACE_CLOSE,
CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN,
CT::T_ARRAY_INDEX_CURLY_BRACE_CLOSE,
CT::T_GROUP_IMPORT_BRACE_OPEN,
CT::T_GROUP_IMPORT_BRACE_CLOSE,
CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN,
CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE,
CT::T_PROPERTY_HOOK_BRACE_OPEN,
CT::T_PROPERTY_HOOK_BRACE_CLOSE,
];
}
/**
* Transform closing `}` for T_CURLY_OPEN into CT::T_CURLY_CLOSE.
*
* This should be done at very beginning of curly braces transformations.
*/
private function transformIntoCurlyCloseBrace(Tokens $tokens, int $index): void
{
$token = $tokens[$index];
if (!$token->isGivenKind(\T_CURLY_OPEN)) {
return;
}
$level = 1;
do {
++$index;
if ($tokens[$index]->equals('{') || $tokens[$index]->isGivenKind(\T_CURLY_OPEN)) { // we count all kind of {
++$level;
} elseif ($tokens[$index]->equals('}')) { // we count all kind of }
--$level;
}
} while (0 < $level);
$tokens[$index] = new Token([CT::T_CURLY_CLOSE, '}']);
}
private function transformIntoDollarCloseBrace(Tokens $tokens, int $index): void
{
$token = $tokens[$index];
if ($token->isGivenKind(\T_DOLLAR_OPEN_CURLY_BRACES)) {
$nextIndex = $tokens->getNextTokenOfKind($index, ['}']);
$tokens[$nextIndex] = new Token([CT::T_DOLLAR_CLOSE_CURLY_BRACES, '}']);
}
}
private function transformIntoDynamicPropBraces(Tokens $tokens, int $index): void
{
$token = $tokens[$index];
if (!$token->isObjectOperator()) {
return;
}
if (!$tokens[$index + 1]->equals('{')) {
return;
}
$openIndex = $index + 1;
$closeIndex = $this->naivelyFindCurlyBlockEnd($tokens, $openIndex);
$tokens[$openIndex] = new Token([CT::T_DYNAMIC_PROP_BRACE_OPEN, '{']);
$tokens[$closeIndex] = new Token([CT::T_DYNAMIC_PROP_BRACE_CLOSE, '}']);
}
private function transformIntoDynamicVarBraces(Tokens $tokens, int $index): void
{
$token = $tokens[$index];
if (!$token->equals('$')) {
return;
}
$openIndex = $tokens->getNextMeaningfulToken($index);
if (null === $openIndex) {
return;
}
$openToken = $tokens[$openIndex];
if (!$openToken->equals('{')) {
return;
}
$closeIndex = $this->naivelyFindCurlyBlockEnd($tokens, $openIndex);
$tokens[$openIndex] = new Token([CT::T_DYNAMIC_VAR_BRACE_OPEN, '{']);
$tokens[$closeIndex] = new Token([CT::T_DYNAMIC_VAR_BRACE_CLOSE, '}']);
}
private function transformIntoPropertyHookBraces(Tokens $tokens, int $index): void
{
if (\PHP_VERSION_ID < 8_04_00) {
return; // @TODO: drop condition when PHP 8.4+ is required or majority of the users are using 8.4+
}
$token = $tokens[$index];
if (!$token->equals('{')) {
return;
}
$nextIndex = $tokens->getNextMeaningfulToken($index);
// skip attributes
while ($tokens[$nextIndex]->isGivenKind(FCT::T_ATTRIBUTE)) {
$nextIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ATTRIBUTE, $nextIndex);
$nextIndex = $tokens->getNextMeaningfulToken($nextIndex);
}
if (!$tokens[$nextIndex]->equalsAny([
[\T_STRING, 'get'],
[\T_STRING, 'set'],
], false)) {
return;
}
$nextNextIndex = $tokens->getNextMeaningfulToken($nextIndex);
if (!$tokens[$nextNextIndex]->equalsAny(['(', '{', ';', [\T_DOUBLE_ARROW]])) {
return;
}
if ($tokens[$nextNextIndex]->equals('(')) {
$closeParenthesisIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $nextNextIndex);
$afterCloseParenthesisIndex = $tokens->getNextMeaningfulToken($closeParenthesisIndex);
if (!$tokens[$afterCloseParenthesisIndex]->equalsAny(['{', [\T_DOUBLE_ARROW]])) {
return;
}
}
$closeIndex = $this->naivelyFindCurlyBlockEnd($tokens, $index);
$tokens[$index] = new Token([CT::T_PROPERTY_HOOK_BRACE_OPEN, '{']);
$tokens[$closeIndex] = new Token([CT::T_PROPERTY_HOOK_BRACE_CLOSE, '}']);
}
private function transformIntoCurlyIndexBraces(Tokens $tokens, int $index): void
{
// Support for fetching array index with braces syntax (`$arr{$index}`)
// was deprecated in 7.4 and removed in 8.0. However, the PHP's behaviour
// differs between 8.0-8.3 (fatal error in runtime) and 8.4 (parse error).
//
// @TODO Do not replace `CT::T_ARRAY_INDEX_CURLY_BRACE_*` for 8.0-8.3, as further optimization
if (\PHP_VERSION_ID >= 8_04_00) {
return;
}
$token = $tokens[$index];
if (!$token->equals('{')) {
return;
}
$prevIndex = $tokens->getPrevMeaningfulToken($index);
if (!$tokens[$prevIndex]->equalsAny([
[\T_STRING],
[\T_VARIABLE],
[CT::T_ARRAY_INDEX_CURLY_BRACE_CLOSE],
']',
')',
])) {
return;
}
if (
$tokens[$prevIndex]->isGivenKind(\T_STRING)
&& !$tokens[$tokens->getPrevMeaningfulToken($prevIndex)]->isObjectOperator()
) {
return;
}
if (
$tokens[$prevIndex]->equals(')')
&& !$tokens[$tokens->getPrevMeaningfulToken(
$tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $prevIndex)
)]->isGivenKind(\T_ARRAY)
) {
return;
}
$closeIndex = $this->naivelyFindCurlyBlockEnd($tokens, $index);
$tokens[$index] = new Token([CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN, '{']);
$tokens[$closeIndex] = new Token([CT::T_ARRAY_INDEX_CURLY_BRACE_CLOSE, '}']);
}
private function transformIntoGroupUseBraces(Tokens $tokens, int $index): void
{
$token = $tokens[$index];
if (!$token->equals('{')) {
return;
}
$prevIndex = $tokens->getPrevMeaningfulToken($index);
if (!$tokens[$prevIndex]->isGivenKind(\T_NS_SEPARATOR)) {
return;
}
$closeIndex = $this->naivelyFindCurlyBlockEnd($tokens, $index);
$tokens[$index] = new Token([CT::T_GROUP_IMPORT_BRACE_OPEN, '{']);
$tokens[$closeIndex] = new Token([CT::T_GROUP_IMPORT_BRACE_CLOSE, '}']);
}
private function transformIntoDynamicClassConstantFetchBraces(Tokens $tokens, int $index): void
{
if (\PHP_VERSION_ID < 8_03_00) {
return; // @TODO: drop condition when PHP 8.3+ is required or majority of the users are using 8.3+
}
$token = $tokens[$index];
if (!$token->equals('{')) {
return;
}
$prevMeaningfulTokenIndex = $tokens->getPrevMeaningfulToken($index);
while (!$tokens[$prevMeaningfulTokenIndex]->isGivenKind(\T_DOUBLE_COLON)) {
if (!$tokens[$prevMeaningfulTokenIndex]->equals(')')) {
return;
}
$prevMeaningfulTokenIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $prevMeaningfulTokenIndex);
$prevMeaningfulTokenIndex = $tokens->getPrevMeaningfulToken($prevMeaningfulTokenIndex);
if (!$tokens[$prevMeaningfulTokenIndex]->equals('}')) {
return;
}
$prevMeaningfulTokenIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $prevMeaningfulTokenIndex);
$prevMeaningfulTokenIndex = $tokens->getPrevMeaningfulToken($prevMeaningfulTokenIndex);
}
$closeIndex = $this->naivelyFindCurlyBlockEnd($tokens, $index);
$nextMeaningfulTokenIndexAfterCloseIndex = $tokens->getNextMeaningfulToken($closeIndex);
if (!$tokens[$nextMeaningfulTokenIndexAfterCloseIndex]->equalsAny([';', [\T_CLOSE_TAG], [\T_DOUBLE_COLON]])) {
return;
}
$tokens[$index] = new Token([CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_OPEN, '{']);
$tokens[$closeIndex] = new Token([CT::T_DYNAMIC_CLASS_CONSTANT_FETCH_CURLY_BRACE_CLOSE, '}']);
}
/**
* We do not want to rely on `$tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index)` here,
* as it relies on block types that are assuming that `}` tokens are already transformed to Custom Tokens that are allowing to distinguish different block types.
* As we are just about to transform `{` and `}` into Custom Tokens by this transformer, thus we need to compare those tokens manually by content without using `Tokens::findBlockEnd`.
*/
private function naivelyFindCurlyBlockEnd(Tokens $tokens, int $startIndex): int
{
if (!$tokens->offsetExists($startIndex)) {
throw new \OutOfBoundsException(\sprintf('Unavailable index: "%s".', $startIndex));
}
if ('{' !== $tokens[$startIndex]->getContent()) {
throw new \InvalidArgumentException(\sprintf('Wrong start index: "%s".', $startIndex));
}
$blockLevel = 1;
$endIndex = $tokens->count() - 1;
for ($index = $startIndex + 1; $index !== $endIndex; ++$index) {
$token = $tokens[$index];
if ('{' === $token->getContent()) {
++$blockLevel;
continue;
}
if ('}' === $token->getContent()) {
--$blockLevel;
if (0 === $blockLevel) {
if (!$token->equals('}')) {
throw new \UnexpectedValueException(\sprintf('Detected block end for index: "%s" was already transformed into other token type: "%s".', $startIndex, $token->getName()));
}
return $index;
}
}
}
throw new \UnexpectedValueException(\sprintf('Missing block end for index: "%s".', $startIndex));
}
}

View File

@@ -0,0 +1,57 @@
<?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\Tokenizer\Transformer;
use PhpCsFixer\Tokenizer\AbstractTransformer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* Transform `class` class' constant from T_CLASS into CT::T_CLASS_CONSTANT.
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class ClassConstantTransformer extends AbstractTransformer
{
public function getRequiredPhpVersionId(): int
{
return 5_05_00;
}
public function process(Tokens $tokens, Token $token, int $index): void
{
if (!$token->equalsAny([
[\T_CLASS, 'class'],
[\T_STRING, 'class'],
], false)) {
return;
}
$prevIndex = $tokens->getPrevMeaningfulToken($index);
$prevToken = $tokens[$prevIndex];
if ($prevToken->isGivenKind(\T_DOUBLE_COLON)) {
$tokens[$index] = new Token([CT::T_CLASS_CONSTANT, $token->getContent()]);
}
}
public function getCustomTokens(): array
{
return [CT::T_CLASS_CONSTANT];
}
}

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\Tokenizer\Transformer;
use PhpCsFixer\Tokenizer\AbstractTransformer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* Transforms for Constructor Property Promotion.
*
* Transform T_PUBLIC, T_PROTECTED and T_PRIVATE of Constructor Property Promotion into custom tokens.
*
* @internal
*/
final class ConstructorPromotionTransformer extends AbstractTransformer
{
public function getRequiredPhpVersionId(): int
{
return 8_00_00;
}
public function process(Tokens $tokens, Token $token, int $index): void
{
if (!$tokens[$index]->isGivenKind(\T_FUNCTION)) {
return;
}
$functionNameIndex = $tokens->getNextMeaningfulToken($index);
if (!$tokens[$functionNameIndex]->isGivenKind(\T_STRING) || '__construct' !== strtolower($tokens[$functionNameIndex]->getContent())) {
return;
}
/** @var int $openParenthesisIndex */
$openParenthesisIndex = $tokens->getNextMeaningfulToken($functionNameIndex); // we are @ '(' now
$closeParenthesisIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openParenthesisIndex);
for ($argsIndex = $openParenthesisIndex; $argsIndex < $closeParenthesisIndex; ++$argsIndex) {
if ($tokens[$argsIndex]->isGivenKind(\T_PUBLIC)) {
$tokens[$argsIndex] = new Token([CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC, $tokens[$argsIndex]->getContent()]);
} elseif ($tokens[$argsIndex]->isGivenKind(\T_PROTECTED)) {
$tokens[$argsIndex] = new Token([CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED, $tokens[$argsIndex]->getContent()]);
} elseif ($tokens[$argsIndex]->isGivenKind(\T_PRIVATE)) {
$tokens[$argsIndex] = new Token([CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE, $tokens[$argsIndex]->getContent()]);
}
}
}
public function getCustomTokens(): array
{
return [
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC,
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED,
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE,
];
}
}

View File

@@ -0,0 +1,65 @@
<?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\Tokenizer\Transformer;
use PhpCsFixer\Tokenizer\AbstractTransformer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* Transform DNF parentheses into CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_OPEN and CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_CLOSE.
*
* @see https://wiki.php.net/rfc/dnf_types
*
* @internal
*/
final class DisjunctiveNormalFormTypeParenthesisTransformer extends AbstractTransformer
{
public function getPriority(): int
{
// needs to run after TypeAlternationTransformer
return -16;
}
public function getRequiredPhpVersionId(): int
{
return 8_02_00;
}
public function process(Tokens $tokens, Token $token, int $index): void
{
if ($token->equals('(') && $tokens[$tokens->getPrevMeaningfulToken($index)]->isGivenKind(CT::T_TYPE_ALTERNATION)) {
$openIndex = $index;
$closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
} elseif ($token->equals(')') && $tokens[$tokens->getNextMeaningfulToken($index)]->isGivenKind(CT::T_TYPE_ALTERNATION)) {
$openIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
$closeIndex = $index;
} else {
return;
}
$tokens[$openIndex] = new Token([CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_OPEN, '(']);
$tokens[$closeIndex] = new Token([CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_CLOSE, ')']);
}
public function getCustomTokens(): array
{
return [
CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_OPEN,
CT::T_DISJUNCTIVE_NORMAL_FORM_TYPE_PARENTHESIS_CLOSE,
];
}
}

View File

@@ -0,0 +1,49 @@
<?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\Tokenizer\Transformer;
use PhpCsFixer\Tokenizer\AbstractTransformer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @internal
*/
final class FirstClassCallableTransformer extends AbstractTransformer
{
public function getRequiredPhpVersionId(): int
{
return 8_01_00;
}
public function process(Tokens $tokens, Token $token, int $index): void
{
if (
$token->isGivenKind(\T_ELLIPSIS)
&& $tokens[$tokens->getPrevMeaningfulToken($index)]->equals('(')
&& $tokens[$tokens->getNextMeaningfulToken($index)]->equals(')')
) {
$tokens[$index] = new Token([CT::T_FIRST_CLASS_CALLABLE, '...']);
}
}
public function getCustomTokens(): array
{
return [
CT::T_FIRST_CLASS_CALLABLE,
];
}
}

View File

@@ -0,0 +1,72 @@
<?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\Tokenizer\Transformer;
use PhpCsFixer\Tokenizer\AbstractTransformer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* Transform const/function import tokens.
*
* Performed transformations:
* - T_CONST into CT::T_CONST_IMPORT
* - T_FUNCTION into CT::T_FUNCTION_IMPORT
*
* @author Gregor Harlan <gharlan@web.de>
*
* @internal
*/
final class ImportTransformer extends AbstractTransformer
{
public function getPriority(): int
{
// Should run after CurlyBraceTransformer and ReturnRefTransformer
return -1;
}
public function getRequiredPhpVersionId(): int
{
return 5_06_00;
}
public function process(Tokens $tokens, Token $token, int $index): void
{
if (!$token->isGivenKind([\T_CONST, \T_FUNCTION])) {
return;
}
$prevToken = $tokens[$tokens->getPrevMeaningfulToken($index)];
if (!$prevToken->isGivenKind(\T_USE)) {
$nextToken = $tokens[$tokens->getNextTokenOfKind($index, ['=', '(', [CT::T_RETURN_REF], [CT::T_GROUP_IMPORT_BRACE_CLOSE]])];
if (!$nextToken->isGivenKind(CT::T_GROUP_IMPORT_BRACE_CLOSE)) {
return;
}
}
$tokens[$index] = new Token([
$token->isGivenKind(\T_FUNCTION) ? CT::T_FUNCTION_IMPORT : CT::T_CONST_IMPORT,
$token->getContent(),
]);
}
public function getCustomTokens(): array
{
return [CT::T_CONST_IMPORT, CT::T_FUNCTION_IMPORT];
}
}

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\Tokenizer\Transformer;
use PhpCsFixer\Tokenizer\AbstractTransformer;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Processor\ImportProcessor;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* Transform NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED and T_NAME_RELATIVE into T_NAMESPACE T_NS_SEPARATOR T_STRING.
*
* @internal
*/
final class NameQualifiedTransformer extends AbstractTransformer
{
public function getPriority(): int
{
return 1; // must run before NamespaceOperatorTransformer
}
public function getRequiredPhpVersionId(): int
{
return 8_00_00;
}
public function process(Tokens $tokens, Token $token, int $index): void
{
if ($token->isGivenKind([FCT::T_NAME_QUALIFIED, FCT::T_NAME_FULLY_QUALIFIED])) {
$this->transformQualified($tokens, $token, $index);
} elseif ($token->isGivenKind(FCT::T_NAME_RELATIVE)) {
$this->transformRelative($tokens, $token, $index);
}
}
public function getCustomTokens(): array
{
return [];
}
private function transformQualified(Tokens $tokens, Token $token, int $index): void
{
$newTokens = ImportProcessor::tokenizeName($token->getContent());
$tokens->overrideRange($index, $index, $newTokens);
}
private function transformRelative(Tokens $tokens, Token $token, int $index): void
{
$newTokens = ImportProcessor::tokenizeName($token->getContent());
$newTokens[0] = new Token([\T_NAMESPACE, 'namespace']);
$tokens->overrideRange($index, $index, $newTokens);
}
}

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\Tokenizer\Transformer;
use PhpCsFixer\Tokenizer\AbstractTransformer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* Transform named argument tokens.
*
* @internal
*/
final class NamedArgumentTransformer extends AbstractTransformer
{
public function getPriority(): int
{
// needs to run after TypeColonTransformer
return -15;
}
public function getRequiredPhpVersionId(): int
{
return 8_00_00;
}
public function process(Tokens $tokens, Token $token, int $index): void
{
if (!$tokens[$index]->equals(':')) {
return;
}
$stringIndex = $tokens->getPrevMeaningfulToken($index);
if (!$tokens[$stringIndex]->isGivenKind(\T_STRING)) {
return;
}
$preStringIndex = $tokens->getPrevMeaningfulToken($stringIndex);
// if equals any [';', '{', '}', [T_OPEN_TAG]] than it is a goto label
// if equals ')' than likely it is a type colon, but sure not a name argument
// if equals '?' than it is part of ternary statement
if (!$tokens[$preStringIndex]->equalsAny([',', '('])) {
return;
}
$tokens[$stringIndex] = new Token([CT::T_NAMED_ARGUMENT_NAME, $tokens[$stringIndex]->getContent()]);
$tokens[$index] = new Token([CT::T_NAMED_ARGUMENT_COLON, ':']);
}
public function getCustomTokens(): array
{
return [
CT::T_NAMED_ARGUMENT_COLON,
CT::T_NAMED_ARGUMENT_NAME,
];
}
}

View File

@@ -0,0 +1,53 @@
<?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\Tokenizer\Transformer;
use PhpCsFixer\Tokenizer\AbstractTransformer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* Transform `namespace` operator from T_NAMESPACE into CT::T_NAMESPACE_OPERATOR.
*
* @author Gregor Harlan <gharlan@web.de>
*
* @internal
*/
final class NamespaceOperatorTransformer extends AbstractTransformer
{
public function getRequiredPhpVersionId(): int
{
return 5_03_00;
}
public function process(Tokens $tokens, Token $token, int $index): void
{
if (!$token->isGivenKind(\T_NAMESPACE)) {
return;
}
$nextIndex = $tokens->getNextMeaningfulToken($index);
if ($tokens[$nextIndex]->isGivenKind(\T_NS_SEPARATOR)) {
$tokens[$index] = new Token([CT::T_NAMESPACE_OPERATOR, $token->getContent()]);
}
}
public function getCustomTokens(): array
{
return [CT::T_NAMESPACE_OPERATOR];
}
}

View File

@@ -0,0 +1,91 @@
<?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\Tokenizer\Transformer;
use PhpCsFixer\Tokenizer\AbstractTransformer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* Transform `?` operator into CT::T_NULLABLE_TYPE in `function foo(?Bar $b) {}`.
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class NullableTypeTransformer extends AbstractTransformer
{
private const TYPES = [
'(',
',',
[CT::T_TYPE_COLON],
[CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC],
[CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED],
[CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE],
[CT::T_ATTRIBUTE_CLOSE],
[\T_PRIVATE],
[\T_PROTECTED],
[\T_PUBLIC],
[\T_VAR],
[\T_STATIC],
[\T_CONST],
[\T_ABSTRACT],
[\T_FINAL],
[FCT::T_READONLY],
[FCT::T_PRIVATE_SET],
[FCT::T_PROTECTED_SET],
[FCT::T_PUBLIC_SET],
];
public function getPriority(): int
{
// needs to run after TypeColonTransformer
return -20;
}
public function getRequiredPhpVersionId(): int
{
return 7_01_00;
}
public function process(Tokens $tokens, Token $token, int $index): void
{
if (!$token->equals('?')) {
return;
}
$prevIndex = $tokens->getPrevMeaningfulToken($index);
if (!$tokens[$prevIndex]->equalsAny(self::TYPES)) {
return;
}
if (
$tokens[$prevIndex]->isGivenKind(\T_STATIC)
&& $tokens[$tokens->getPrevMeaningfulToken($prevIndex)]->isGivenKind(\T_INSTANCEOF)
) {
return;
}
$tokens[$index] = new Token([CT::T_NULLABLE_TYPE, '?']);
}
public function getCustomTokens(): array
{
return [CT::T_NULLABLE_TYPE];
}
}

View File

@@ -0,0 +1,47 @@
<?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\Tokenizer\Transformer;
use PhpCsFixer\Tokenizer\AbstractTransformer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* Transform `&` operator into CT::T_RETURN_REF in `function & foo() {}`.
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class ReturnRefTransformer extends AbstractTransformer
{
public function getRequiredPhpVersionId(): int
{
return 5_00_00;
}
public function process(Tokens $tokens, Token $token, int $index): void
{
if ($token->equals('&') && $tokens[$tokens->getPrevMeaningfulToken($index)]->isGivenKind([\T_FUNCTION, \T_FN])) {
$tokens[$index] = new Token([CT::T_RETURN_REF, '&']);
}
}
public function getCustomTokens(): array
{
return [CT::T_RETURN_REF];
}
}

View File

@@ -0,0 +1,183 @@
<?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\Tokenizer\Transformer;
use PhpCsFixer\Tokenizer\AbstractTransformer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* Transform discriminate overloaded square braces tokens.
*
* Performed transformations:
* - in `[1, 2, 3]` into CT::T_ARRAY_SQUARE_BRACE_OPEN and CT::T_ARRAY_SQUARE_BRACE_CLOSE,
* - in `[$a, &$b, [$c]] = array(1, 2, array(3))` into CT::T_DESTRUCTURING_SQUARE_BRACE_OPEN and CT::T_DESTRUCTURING_SQUARE_BRACE_CLOSE.
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class SquareBraceTransformer extends AbstractTransformer
{
public function getPriority(): int
{
// must run after CurlyBraceTransformer and AttributeTransformer
return -1;
}
public function getRequiredPhpVersionId(): int
{
// Short array syntax was introduced in PHP 5.4, but the fixer is smart
// enough to handle it even before 5.4.
// Same for array destructing syntax sugar `[` introduced in PHP 7.1.
return 5_00_00;
}
public function process(Tokens $tokens, Token $token, int $index): void
{
if ($this->isArrayDestructing($tokens, $index)) {
$this->transformIntoDestructuringSquareBrace($tokens, $index);
return;
}
if ($this->isShortArray($tokens, $index)) {
$this->transformIntoArraySquareBrace($tokens, $index);
}
}
public function getCustomTokens(): array
{
return [
CT::T_ARRAY_SQUARE_BRACE_OPEN,
CT::T_ARRAY_SQUARE_BRACE_CLOSE,
CT::T_DESTRUCTURING_SQUARE_BRACE_OPEN,
CT::T_DESTRUCTURING_SQUARE_BRACE_CLOSE,
];
}
private function transformIntoArraySquareBrace(Tokens $tokens, int $index): void
{
$endIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_INDEX_SQUARE_BRACE, $index);
$tokens[$index] = new Token([CT::T_ARRAY_SQUARE_BRACE_OPEN, '[']);
$tokens[$endIndex] = new Token([CT::T_ARRAY_SQUARE_BRACE_CLOSE, ']']);
}
private function transformIntoDestructuringSquareBrace(Tokens $tokens, int $index): void
{
$endIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_INDEX_SQUARE_BRACE, $index);
$tokens[$index] = new Token([CT::T_DESTRUCTURING_SQUARE_BRACE_OPEN, '[']);
$tokens[$endIndex] = new Token([CT::T_DESTRUCTURING_SQUARE_BRACE_CLOSE, ']']);
$previousMeaningfulIndex = $index;
$index = $tokens->getNextMeaningfulToken($index);
while ($index < $endIndex) {
if ($tokens[$index]->equals('[') && $tokens[$previousMeaningfulIndex]->equalsAny([[CT::T_DESTRUCTURING_SQUARE_BRACE_OPEN], ','])) {
$tokens[$tokens->findBlockEnd(Tokens::BLOCK_TYPE_INDEX_SQUARE_BRACE, $index)] = new Token([CT::T_DESTRUCTURING_SQUARE_BRACE_CLOSE, ']']);
$tokens[$index] = new Token([CT::T_DESTRUCTURING_SQUARE_BRACE_OPEN, '[']);
}
$previousMeaningfulIndex = $index;
$index = $tokens->getNextMeaningfulToken($index);
}
}
/**
* Check if token under given index is short array opening.
*/
private function isShortArray(Tokens $tokens, int $index): bool
{
if (!$tokens[$index]->equals('[')) {
return false;
}
$prevToken = $tokens[$tokens->getPrevMeaningfulToken($index)];
if ($prevToken->equalsAny([
')',
']',
'}',
'"',
[\T_CONSTANT_ENCAPSED_STRING],
[\T_STRING],
[\T_STRING_VARNAME],
[\T_VARIABLE],
[CT::T_ARRAY_SQUARE_BRACE_CLOSE],
[CT::T_DYNAMIC_PROP_BRACE_CLOSE],
[CT::T_DYNAMIC_VAR_BRACE_CLOSE],
[CT::T_ARRAY_INDEX_CURLY_BRACE_CLOSE],
])) {
return false;
}
$nextToken = $tokens[$tokens->getNextMeaningfulToken($index)];
if ($nextToken->equals(']')) {
return true;
}
return !$this->isArrayDestructing($tokens, $index);
}
private function isArrayDestructing(Tokens $tokens, int $index): bool
{
if (!$tokens[$index]->equals('[')) {
return false;
}
$prevIndex = $tokens->getPrevMeaningfulToken($index);
$prevToken = $tokens[$prevIndex];
if ($prevToken->equalsAny([
')',
']',
'"',
[\T_CONSTANT_ENCAPSED_STRING],
[\T_STRING],
[\T_STRING_VARNAME],
[\T_VARIABLE],
[CT::T_ARRAY_SQUARE_BRACE_CLOSE],
[CT::T_DYNAMIC_PROP_BRACE_CLOSE],
[CT::T_DYNAMIC_VAR_BRACE_CLOSE],
[CT::T_ARRAY_INDEX_CURLY_BRACE_CLOSE],
])) {
return false;
}
if ($prevToken->isGivenKind(\T_AS)) {
return true;
}
if ($prevToken->isGivenKind(\T_DOUBLE_ARROW)) {
$variableIndex = $tokens->getPrevMeaningfulToken($prevIndex);
if (!$tokens[$variableIndex]->isGivenKind(\T_VARIABLE)) {
return false;
}
$prevVariableIndex = $tokens->getPrevMeaningfulToken($variableIndex);
if ($tokens[$prevVariableIndex]->isGivenKind(\T_AS)) {
return true;
}
}
$type = Tokens::detectBlockType($tokens[$index]);
$end = $tokens->findBlockEnd($type['type'], $index);
$nextToken = $tokens[$tokens->getNextMeaningfulToken($end)];
return $nextToken->equals('=');
}
}

View File

@@ -0,0 +1,57 @@
<?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\Tokenizer\Transformer;
use PhpCsFixer\Tokenizer\AbstractTypeTransformer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* Transform `|` operator into CT::T_TYPE_ALTERNATION in `function foo(Type1 | Type2 $x) {`
* or `} catch (ExceptionType1 | ExceptionType2 $e) {`.
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class TypeAlternationTransformer extends AbstractTypeTransformer
{
public function getPriority(): int
{
// needs to run after ArrayTypehintTransformer, TypeColonTransformer and AttributeTransformer
return -15;
}
public function getRequiredPhpVersionId(): int
{
return 7_01_00;
}
public function process(Tokens $tokens, Token $token, int $index): void
{
$this->doProcess($tokens, $index, '|');
}
public function getCustomTokens(): array
{
return [CT::T_TYPE_ALTERNATION];
}
protected function replaceToken(Tokens $tokens, int $index): void
{
$tokens[$index] = new Token([CT::T_TYPE_ALTERNATION, '|']);
}
}

View File

@@ -0,0 +1,81 @@
<?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\Tokenizer\Transformer;
use PhpCsFixer\Tokenizer\AbstractTransformer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* Transform `:` operator into CT::T_TYPE_COLON in `function foo() : int {}`.
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class TypeColonTransformer extends AbstractTransformer
{
public function getPriority(): int
{
// needs to run after ReturnRefTransformer and UseTransformer
// and before TypeAlternationTransformer
return -10;
}
public function getRequiredPhpVersionId(): int
{
return 7_00_00;
}
public function process(Tokens $tokens, Token $token, int $index): void
{
if (!$token->equals(':')) {
return;
}
$endIndex = $tokens->getPrevMeaningfulToken($index);
if ($tokens[$tokens->getPrevMeaningfulToken($endIndex)]->isGivenKind(FCT::T_ENUM)) {
$tokens[$index] = new Token([CT::T_TYPE_COLON, ':']);
return;
}
if (!$tokens[$endIndex]->equals(')')) {
return;
}
$startIndex = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $endIndex);
$prevIndex = $tokens->getPrevMeaningfulToken($startIndex);
$prevToken = $tokens[$prevIndex];
// if this could be a function name we need to take one more step
if ($prevToken->isGivenKind(\T_STRING)) {
$prevIndex = $tokens->getPrevMeaningfulToken($prevIndex);
$prevToken = $tokens[$prevIndex];
}
if ($prevToken->isGivenKind([\T_FUNCTION, CT::T_RETURN_REF, CT::T_USE_LAMBDA, \T_FN])) {
$tokens[$index] = new Token([CT::T_TYPE_COLON, ':']);
}
}
public function getCustomTokens(): array
{
return [CT::T_TYPE_COLON];
}
}

View File

@@ -0,0 +1,55 @@
<?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\Tokenizer\Transformer;
use PhpCsFixer\Tokenizer\AbstractTypeTransformer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* Transform `&` operator into CT::T_TYPE_INTERSECTION in `function foo(Type1 & Type2 $x) {`
* or `} catch (ExceptionType1 & ExceptionType2 $e) {`.
*
* @internal
*/
final class TypeIntersectionTransformer extends AbstractTypeTransformer
{
public function getPriority(): int
{
// needs to run after ArrayTypehintTransformer, TypeColonTransformer and AttributeTransformer
return -15;
}
public function getRequiredPhpVersionId(): int
{
return 8_01_00;
}
public function process(Tokens $tokens, Token $token, int $index): void
{
$this->doProcess($tokens, $index, [\T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG, '&']);
}
public function getCustomTokens(): array
{
return [CT::T_TYPE_INTERSECTION];
}
protected function replaceToken(Tokens $tokens, int $index): void
{
$tokens[$index] = new Token([CT::T_TYPE_INTERSECTION, '&']);
}
}

View File

@@ -0,0 +1,99 @@
<?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\Tokenizer\Transformer;
use PhpCsFixer\Tokenizer\AbstractTransformer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\FCT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* Transform T_USE into:
* - CT::T_USE_TRAIT for imports,
* - CT::T_USE_LAMBDA for lambda variable uses.
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class UseTransformer extends AbstractTransformer
{
private const CLASS_TYPES = [\T_TRAIT, FCT::T_ENUM];
public function getPriority(): int
{
// Should run after CurlyBraceTransformer and before TypeColonTransformer
return -5;
}
public function getRequiredPhpVersionId(): int
{
return 5_03_00;
}
public function process(Tokens $tokens, Token $token, int $index): void
{
if ($token->isGivenKind(\T_USE) && $this->isUseForLambda($tokens, $index)) {
$tokens[$index] = new Token([CT::T_USE_LAMBDA, $token->getContent()]);
return;
}
// Only search inside class/trait body for `T_USE` for traits.
// Cannot import traits inside interfaces or anywhere else
if ($token->isGivenKind(\T_CLASS)) {
if ($tokens[$tokens->getPrevMeaningfulToken($index)]->isGivenKind(\T_DOUBLE_COLON)) {
return;
}
} elseif (!$token->isGivenKind(self::CLASS_TYPES)) {
return;
}
$index = $tokens->getNextTokenOfKind($index, ['{']);
$innerLimit = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
while ($index < $innerLimit) {
$token = $tokens[++$index];
if (!$token->isGivenKind(\T_USE)) {
continue;
}
if ($this->isUseForLambda($tokens, $index)) {
$tokens[$index] = new Token([CT::T_USE_LAMBDA, $token->getContent()]);
} else {
$tokens[$index] = new Token([CT::T_USE_TRAIT, $token->getContent()]);
}
}
}
public function getCustomTokens(): array
{
return [CT::T_USE_TRAIT, CT::T_USE_LAMBDA];
}
/**
* Check if token under given index is `use` statement for lambda function.
*/
private function isUseForLambda(Tokens $tokens, int $index): bool
{
$nextToken = $tokens[$tokens->getNextMeaningfulToken($index)];
// test `function () use ($foo) {}` case
return $nextToken->equals('(');
}
}

View File

@@ -0,0 +1,64 @@
<?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\Tokenizer\Transformer;
use PhpCsFixer\Tokenizer\AbstractTransformer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* Move trailing whitespaces from comments and docs into following T_WHITESPACE token.
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class WhitespacyCommentTransformer extends AbstractTransformer
{
public function getRequiredPhpVersionId(): int
{
return 5_00_00;
}
public function process(Tokens $tokens, Token $token, int $index): void
{
if (!$token->isComment()) {
return;
}
$content = $token->getContent();
$trimmedContent = rtrim($content);
// nothing trimmed, nothing to do
if ($content === $trimmedContent) {
return;
}
$whitespaces = substr($content, \strlen($trimmedContent));
$tokens[$index] = new Token([$token->getId(), $trimmedContent]);
if (isset($tokens[$index + 1]) && $tokens[$index + 1]->isWhitespace()) {
$tokens[$index + 1] = new Token([\T_WHITESPACE, $whitespaces.$tokens[$index + 1]->getContent()]);
} else {
$tokens->insertAt($index + 1, new Token([\T_WHITESPACE, $whitespaces]));
}
}
public function getCustomTokens(): array
{
return [];
}
}

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\Tokenizer;
/**
* Interface for Transformer class.
*
* Transformer role is to register custom tokens and transform Tokens collection to use them.
*
* Custom token is a user defined token type and is used to separate different meaning of original token type.
* For example T_ARRAY is a token for both creating new array and typehinting a parameter. This two meaning should have two token types.
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
interface TransformerInterface
{
/**
* Get tokens created by Transformer.
*
* @return list<int>
*/
public function getCustomTokens(): array;
/**
* Return the name of the transformer.
*
* The name must be all lowercase and without any spaces.
*
* @return string The name of the fixer
*/
public function getName(): string;
/**
* Returns the priority of the transformer.
*
* The default priority is 0 and higher priorities are executed first.
*/
public function getPriority(): int;
/**
* Return minimal required PHP version id to transform the code.
*
* Custom Token kinds from Transformers are always registered, but sometimes
* there is no need to analyse the Tokens if for sure we cannot find examined
* token kind, e.g. transforming `T_FUNCTION` in `<?php use function Foo\\bar;`
* code.
*/
public function getRequiredPhpVersionId(): int;
/**
* Process Token to transform it into custom token when needed.
*/
public function process(Tokens $tokens, Token $token, int $index): void;
}

View File

@@ -0,0 +1,111 @@
<?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\Tokenizer;
use Symfony\Component\Finder\Finder;
/**
* Collection of Transformer classes.
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class Transformers
{
/**
* The registered transformers.
*
* @var list<TransformerInterface>
*/
private array $items = [];
/**
* Register built in Transformers.
*/
private function __construct()
{
$this->registerBuiltInTransformers();
usort($this->items, static fn (TransformerInterface $a, TransformerInterface $b): int => $b->getPriority() <=> $a->getPriority());
}
public static function createSingleton(): self
{
static $instance = null;
if (!$instance) {
$instance = new self();
}
return $instance;
}
/**
* Transform given Tokens collection through all Transformer classes.
*
* @param Tokens $tokens Tokens collection
*/
public function transform(Tokens $tokens): void
{
foreach ($this->items as $transformer) {
foreach ($tokens as $index => $token) {
$transformer->process($tokens, $token, $index);
}
}
}
/**
* @param TransformerInterface $transformer Transformer
*/
private function registerTransformer(TransformerInterface $transformer): void
{
if (\PHP_VERSION_ID >= $transformer->getRequiredPhpVersionId()) {
$this->items[] = $transformer;
}
}
private function registerBuiltInTransformers(): void
{
static $registered = false;
if ($registered) {
return;
}
$registered = true;
foreach ($this->findBuiltInTransformers() as $transformer) {
$this->registerTransformer($transformer);
}
}
/**
* @return \Generator<TransformerInterface>
*/
private function findBuiltInTransformers(): iterable
{
foreach (Finder::create()->files()->in(__DIR__.'/Transformer') as $file) {
$relativeNamespace = $file->getRelativePath();
$class = __NAMESPACE__.'\Transformer\\'.('' !== $relativeNamespace ? $relativeNamespace.'\\' : '').$file->getBasename('.php');
$instance = new $class();
\assert($instance instanceof TransformerInterface);
yield $instance;
}
}
}