Missing dependancies

This commit is contained in:
tokslaw7
2023-06-21 12:19:22 +00:00
parent fbf3f180c2
commit 421f25c80d
2356 changed files with 342670 additions and 4743 deletions
@@ -0,0 +1,238 @@
<?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;
use PhpCsFixer\Doctrine\Annotation\Tokens as DoctrineAnnotationTokens;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;
/**
* @internal
*/
abstract class AbstractDoctrineAnnotationFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/**
* @var array<int, array{classIndex: int, token: Token, type: string}>
*/
private array $classyElements;
/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(T_DOC_COMMENT);
}
/**
* {@inheritdoc}
*/
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
// fetch indices one time, this is safe as we never add or remove a token during fixing
$analyzer = new TokensAnalyzer($tokens);
$this->classyElements = $analyzer->getClassyElements();
/** @var Token $docCommentToken */
foreach ($tokens->findGivenKind(T_DOC_COMMENT) as $index => $docCommentToken) {
if (!$this->nextElementAcceptsDoctrineAnnotations($tokens, $index)) {
continue;
}
$doctrineAnnotationTokens = DoctrineAnnotationTokens::createFromDocComment(
$docCommentToken,
$this->configuration['ignored_tags']
);
$this->fixAnnotations($doctrineAnnotationTokens);
$tokens[$index] = new Token([T_DOC_COMMENT, $doctrineAnnotationTokens->getCode()]);
}
}
/**
* Fixes Doctrine annotations from the given PHPDoc style comment.
*/
abstract protected function fixAnnotations(DoctrineAnnotationTokens $doctrineAnnotationTokens): void;
/**
* {@inheritdoc}
*/
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('ignored_tags', 'List of tags that must not be treated as Doctrine Annotations.'))
->setAllowedTypes(['array'])
->setAllowedValues([static function (array $values): bool {
foreach ($values as $value) {
if (!\is_string($value)) {
return false;
}
}
return true;
}])
->setDefault([
// PHPDocumentor 1
'abstract',
'access',
'code',
'deprec',
'encode',
'exception',
'final',
'ingroup',
'inheritdoc',
'inheritDoc',
'magic',
'name',
'toc',
'tutorial',
'private',
'static',
'staticvar',
'staticVar',
'throw',
// PHPDocumentor 2
'api',
'author',
'category',
'copyright',
'deprecated',
'example',
'filesource',
'global',
'ignore',
'internal',
'license',
'link',
'method',
'package',
'param',
'property',
'property-read',
'property-write',
'return',
'see',
'since',
'source',
'subpackage',
'throws',
'todo',
'TODO',
'usedBy',
'uses',
'var',
'version',
// PHPUnit
'after',
'afterClass',
'backupGlobals',
'backupStaticAttributes',
'before',
'beforeClass',
'codeCoverageIgnore',
'codeCoverageIgnoreStart',
'codeCoverageIgnoreEnd',
'covers',
'coversDefaultClass',
'coversNothing',
'dataProvider',
'depends',
'expectedException',
'expectedExceptionCode',
'expectedExceptionMessage',
'expectedExceptionMessageRegExp',
'group',
'large',
'medium',
'preserveGlobalState',
'requires',
'runTestsInSeparateProcesses',
'runInSeparateProcess',
'small',
'test',
'testdox',
'ticket',
'uses',
// PHPCheckStyle
'SuppressWarnings',
// PHPStorm
'noinspection',
// PEAR
'package_version',
// PlantUML
'enduml',
'startuml',
// Psalm
'psalm',
// PHPStan
'phpstan',
'template',
// other
'fix',
'FIXME',
'fixme',
'override',
])
->getOption(),
]);
}
private function nextElementAcceptsDoctrineAnnotations(Tokens $tokens, int $index): bool
{
do {
$index = $tokens->getNextMeaningfulToken($index);
if (null === $index) {
return false;
}
} while ($tokens[$index]->isGivenKind([T_ABSTRACT, T_FINAL]));
if ($tokens[$index]->isGivenKind(T_CLASS)) {
return true;
}
$modifierKinds = [T_PUBLIC, T_PROTECTED, T_PRIVATE, T_FINAL, T_ABSTRACT, T_NS_SEPARATOR, T_STRING, CT::T_NULLABLE_TYPE];
if (\defined('T_READONLY')) { // @TODO: drop condition when PHP 8.1+ is required
$modifierKinds[] = T_READONLY;
}
while ($tokens[$index]->isGivenKind($modifierKinds)) {
$index = $tokens->getNextMeaningfulToken($index);
}
if (!isset($this->classyElements[$index])) {
return false;
}
return $tokens[$this->classyElements[$index]['classIndex']]->isGivenKind(T_CLASS); // interface, enums and traits cannot have doctrine annotations
}
}
@@ -0,0 +1,206 @@
<?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;
use PhpCsFixer\ConfigurationException\InvalidFixerConfigurationException;
use PhpCsFixer\ConfigurationException\InvalidForEnvFixerConfigurationException;
use PhpCsFixer\ConfigurationException\RequiredFixerConfigurationException;
use PhpCsFixer\Console\Application;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\FixerInterface;
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
use PhpCsFixer\FixerConfiguration\DeprecatedFixerOption;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
use PhpCsFixer\FixerConfiguration\InvalidOptionsForEnvException;
use PhpCsFixer\Tokenizer\Tokens;
use Symfony\Component\OptionsResolver\Exception\ExceptionInterface;
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
abstract class AbstractFixer implements FixerInterface
{
/**
* @var null|array<string, mixed>
*/
protected $configuration;
/**
* @var WhitespacesFixerConfig
*/
protected $whitespacesConfig;
/**
* @var null|FixerConfigurationResolverInterface
*/
private $configurationDefinition;
public function __construct()
{
if ($this instanceof ConfigurableFixerInterface) {
try {
$this->configure([]);
} catch (RequiredFixerConfigurationException $e) {
// ignore
}
}
if ($this instanceof WhitespacesAwareFixerInterface) {
$this->whitespacesConfig = $this->getDefaultWhitespacesFixerConfig();
}
}
final public function fix(\SplFileInfo $file, Tokens $tokens): void
{
if ($this instanceof ConfigurableFixerInterface && null === $this->configuration) {
throw new RequiredFixerConfigurationException($this->getName(), 'Configuration is required.');
}
if (0 < $tokens->count() && $this->isCandidate($tokens) && $this->supports($file)) {
$this->applyFix($file, $tokens);
}
}
/**
* {@inheritdoc}
*/
public function isRisky(): bool
{
return false;
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
$nameParts = explode('\\', static::class);
$name = substr(end($nameParts), 0, -\strlen('Fixer'));
return Utils::camelCaseToUnderscore($name);
}
/**
* {@inheritdoc}
*/
public function getPriority(): int
{
return 0;
}
/**
* {@inheritdoc}
*/
public function supports(\SplFileInfo $file): bool
{
return true;
}
/**
* @param array<string, mixed> $configuration
*/
public function configure(array $configuration): void
{
if (!$this instanceof ConfigurableFixerInterface) {
throw new \LogicException('Cannot configure using Abstract parent, child not implementing "PhpCsFixer\Fixer\ConfigurableFixerInterface".');
}
foreach ($this->getConfigurationDefinition()->getOptions() as $option) {
if (!$option instanceof DeprecatedFixerOption) {
continue;
}
$name = $option->getName();
if (\array_key_exists($name, $configuration)) {
Utils::triggerDeprecation(new \InvalidArgumentException(sprintf(
'Option "%s" for rule "%s" is deprecated and will be removed in version %d.0. %s',
$name,
$this->getName(),
Application::getMajorVersion() + 1,
str_replace('`', '"', $option->getDeprecationMessage())
)));
}
}
try {
$this->configuration = $this->getConfigurationDefinition()->resolve($configuration);
} catch (MissingOptionsException $exception) {
throw new RequiredFixerConfigurationException(
$this->getName(),
sprintf('Missing required configuration: %s', $exception->getMessage()),
$exception
);
} catch (InvalidOptionsForEnvException $exception) {
throw new InvalidForEnvFixerConfigurationException(
$this->getName(),
sprintf('Invalid configuration for env: %s', $exception->getMessage()),
$exception
);
} catch (ExceptionInterface $exception) {
throw new InvalidFixerConfigurationException(
$this->getName(),
sprintf('Invalid configuration: %s', $exception->getMessage()),
$exception
);
}
}
public function getConfigurationDefinition(): FixerConfigurationResolverInterface
{
if (!$this instanceof ConfigurableFixerInterface) {
throw new \LogicException(sprintf('Cannot get configuration definition using Abstract parent, child "%s" not implementing "PhpCsFixer\Fixer\ConfigurableFixerInterface".', static::class));
}
if (null === $this->configurationDefinition) {
$this->configurationDefinition = $this->createConfigurationDefinition();
}
return $this->configurationDefinition;
}
public function setWhitespacesConfig(WhitespacesFixerConfig $config): void
{
if (!$this instanceof WhitespacesAwareFixerInterface) {
throw new \LogicException('Cannot run method for class not implementing "PhpCsFixer\Fixer\WhitespacesAwareFixerInterface".');
}
$this->whitespacesConfig = $config;
}
abstract protected function applyFix(\SplFileInfo $file, Tokens $tokens): void;
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
if (!$this instanceof ConfigurableFixerInterface) {
throw new \LogicException('Cannot create configuration definition using Abstract parent, child not implementing "PhpCsFixer\Fixer\ConfigurableFixerInterface".');
}
throw new \LogicException('Not implemented.');
}
private function getDefaultWhitespacesFixerConfig(): WhitespacesFixerConfig
{
static $defaultWhitespacesFixerConfig = null;
if (null === $defaultWhitespacesFixerConfig) {
$defaultWhitespacesFixerConfig = new WhitespacesFixerConfig(' ', "\n");
}
return $defaultWhitespacesFixerConfig;
}
}
@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer;
use PhpCsFixer\Tokenizer\Analyzer\ArgumentsAnalyzer;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @internal
*/
abstract class AbstractFopenFlagFixer extends AbstractFunctionReferenceFixer
{
/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAllTokenKindsFound([T_STRING, T_CONSTANT_ENCAPSED_STRING]);
}
/**
* {@inheritdoc}
*/
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$argumentsAnalyzer = new ArgumentsAnalyzer();
$index = 0;
$end = $tokens->count() - 1;
while (true) {
$candidate = $this->find('fopen', $tokens, $index, $end);
if (null === $candidate) {
break;
}
$index = $candidate[1]; // proceed to '(' of `fopen`
// fetch arguments
$arguments = $argumentsAnalyzer->getArguments(
$tokens,
$index,
$candidate[2]
);
$argumentsCount = \count($arguments); // argument count sanity check
if ($argumentsCount < 2 || $argumentsCount > 4) {
continue;
}
$argumentStartIndex = array_keys($arguments)[1]; // get second argument index
$this->fixFopenFlagToken(
$tokens,
$argumentStartIndex,
$arguments[$argumentStartIndex]
);
}
}
abstract protected function fixFopenFlagToken(Tokens $tokens, int $argumentStartIndex, int $argumentEndIndex): void;
protected function isValidModeString(string $mode): bool
{
$modeLength = \strlen($mode);
if ($modeLength < 1 || $modeLength > 13) { // 13 === length 'r+w+a+x+c+etb'
return false;
}
$validFlags = [
'a' => true,
'b' => true,
'c' => true,
'e' => true,
'r' => true,
't' => true,
'w' => true,
'x' => true,
];
if (!isset($validFlags[$mode[0]])) {
return false;
}
unset($validFlags[$mode[0]]);
for ($i = 1; $i < $modeLength; ++$i) {
if (isset($validFlags[$mode[$i]])) {
unset($validFlags[$mode[$i]]);
continue;
}
if ('+' !== $mode[$i]
|| (
'a' !== $mode[$i - 1] // 'a+','c+','r+','w+','x+'
&& 'c' !== $mode[$i - 1]
&& 'r' !== $mode[$i - 1]
&& 'w' !== $mode[$i - 1]
&& 'x' !== $mode[$i - 1]
)
) {
return false;
}
}
return true;
}
}
@@ -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;
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @internal
*
* @author Vladimir Reznichenko <kalessil@gmail.com>
*/
abstract class AbstractFunctionReferenceFixer extends AbstractFixer
{
/**
* @var null|FunctionsAnalyzer
*/
private $functionsAnalyzer;
/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(T_STRING);
}
/**
* {@inheritdoc}
*/
public function isRisky(): bool
{
return true;
}
/**
* Looks up Tokens sequence for suitable candidates and delivers boundaries information,
* which can be supplied by other methods in this abstract class.
*
* @return null|int[] returns $functionName, $openParenthesis, $closeParenthesis packed into array
*/
protected function find(string $functionNameToSearch, Tokens $tokens, int $start = 0, ?int $end = null): ?array
{
if (null === $this->functionsAnalyzer) {
$this->functionsAnalyzer = new FunctionsAnalyzer();
}
// make interface consistent with findSequence
$end ??= $tokens->count();
// find raw sequence which we can analyse for context
$candidateSequence = [[T_STRING, $functionNameToSearch], '('];
$matches = $tokens->findSequence($candidateSequence, $start, $end, false);
if (null === $matches) {
return null; // not found, simply return without further attempts
}
// translate results for humans
[$functionName, $openParenthesis] = array_keys($matches);
if (!$this->functionsAnalyzer->isGlobalFunctionCall($tokens, $functionName)) {
return $this->find($functionNameToSearch, $tokens, $openParenthesis, $end);
}
return [$functionName, $openParenthesis, $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openParenthesis)];
}
}
@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer;
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* This abstract fixer is responsible for ensuring that a certain number of
* lines prefix a namespace declaration.
*
* @author Graham Campbell <hello@gjcampbell.co.uk>
*
* @internal
*/
abstract class AbstractLinesBeforeNamespaceFixer extends AbstractFixer implements WhitespacesAwareFixerInterface
{
/**
* Make sure # of line breaks prefixing namespace is within given range.
*
* @param int $expectedMin min. # of line breaks
* @param int $expectedMax max. # of line breaks
*/
protected function fixLinesBeforeNamespace(Tokens $tokens, int $index, int $expectedMin, int $expectedMax): void
{
// Let's determine the total numbers of new lines before the namespace
// and the opening token
$openingTokenIndex = null;
$precedingNewlines = 0;
$newlineInOpening = false;
$openingToken = null;
for ($i = 1; $i <= 2; ++$i) {
if (isset($tokens[$index - $i])) {
$token = $tokens[$index - $i];
if ($token->isGivenKind(T_OPEN_TAG)) {
$openingToken = $token;
$openingTokenIndex = $index - $i;
$newlineInOpening = str_contains($token->getContent(), "\n");
if ($newlineInOpening) {
++$precedingNewlines;
}
break;
}
if (false === $token->isGivenKind(T_WHITESPACE)) {
break;
}
$precedingNewlines += substr_count($token->getContent(), "\n");
}
}
if ($precedingNewlines >= $expectedMin && $precedingNewlines <= $expectedMax) {
return;
}
$previousIndex = $index - 1;
$previous = $tokens[$previousIndex];
if (0 === $expectedMax) {
// Remove all the previous new lines
if ($previous->isWhitespace()) {
$tokens->clearAt($previousIndex);
}
// Remove new lines in opening token
if ($newlineInOpening) {
$tokens[$openingTokenIndex] = new Token([T_OPEN_TAG, rtrim($openingToken->getContent()).' ']);
}
return;
}
$lineEnding = $this->whitespacesConfig->getLineEnding();
$newlinesForWhitespaceToken = $expectedMax;
if (null !== $openingToken) {
// Use the configured line ending for the PHP opening tag
$content = rtrim($openingToken->getContent());
$newContent = $content.$lineEnding;
$tokens[$openingTokenIndex] = new Token([T_OPEN_TAG, $newContent]);
--$newlinesForWhitespaceToken;
}
if (0 === $newlinesForWhitespaceToken) {
// We have all the needed new lines in the opening tag
if ($previous->isWhitespace()) {
// Let's remove the previous token containing extra new lines
$tokens->clearAt($previousIndex);
}
return;
}
if ($previous->isWhitespace()) {
// Fix the previous whitespace token
$tokens[$previousIndex] = new Token([T_WHITESPACE, str_repeat($lineEnding, $newlinesForWhitespaceToken).substr($previous->getContent(), strrpos($previous->getContent(), "\n") + 1)]);
} else {
// Add a new whitespace token
$tokens->insertAt($index, new Token([T_WHITESPACE, str_repeat($lineEnding, $newlinesForWhitespaceToken)]));
}
}
}
@@ -0,0 +1,207 @@
<?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;
use PhpCsFixer\Tokenizer\Tokens;
abstract class AbstractNoUselessElseFixer extends AbstractFixer
{
/**
* {@inheritdoc}
*/
public function getPriority(): int
{
// should be run before NoWhitespaceInBlankLineFixer, NoExtraBlankLinesFixer, BracesFixer and after NoEmptyStatementFixer.
return 39;
}
protected function isSuperfluousElse(Tokens $tokens, int $index): bool
{
$previousBlockStart = $index;
do {
// Check if all 'if', 'else if ' and 'elseif' blocks above this 'else' always end,
// if so this 'else' is overcomplete.
[$previousBlockStart, $previousBlockEnd] = $this->getPreviousBlock($tokens, $previousBlockStart);
// short 'if' detection
$previous = $previousBlockEnd;
if ($tokens[$previous]->equals('}')) {
$previous = $tokens->getPrevMeaningfulToken($previous);
}
if (
!$tokens[$previous]->equals(';') // 'if' block doesn't end with semicolon, keep 'else'
|| $tokens[$tokens->getPrevMeaningfulToken($previous)]->equals('{') // empty 'if' block, keep 'else'
) {
return false;
}
$candidateIndex = $tokens->getPrevTokenOfKind(
$previous,
[
';',
[T_BREAK],
[T_CLOSE_TAG],
[T_CONTINUE],
[T_EXIT],
[T_GOTO],
[T_IF],
[T_RETURN],
[T_THROW],
]
);
if (null === $candidateIndex || $tokens[$candidateIndex]->equalsAny([';', [T_CLOSE_TAG], [T_IF]])) {
return false;
}
if ($tokens[$candidateIndex]->isGivenKind(T_THROW)) {
$previousIndex = $tokens->getPrevMeaningfulToken($candidateIndex);
if (!$tokens[$previousIndex]->equalsAny([';', '{'])) {
return false;
}
}
if ($this->isInConditional($tokens, $candidateIndex, $previousBlockStart)
|| $this->isInConditionWithoutBraces($tokens, $candidateIndex, $previousBlockStart)
) {
return false;
}
// implicit continue, i.e. delete candidate
} while (!$tokens[$previousBlockStart]->isGivenKind(T_IF));
return true;
}
/**
* Return the first and last token index of the previous block.
*
* [0] First is either T_IF, T_ELSE or T_ELSEIF
* [1] Last is either '}' or ';' / T_CLOSE_TAG for short notation blocks
*
* @param int $index T_IF, T_ELSE, T_ELSEIF
*
* @return int[]
*/
private function getPreviousBlock(Tokens $tokens, int $index): array
{
$close = $previous = $tokens->getPrevMeaningfulToken($index);
// short 'if' detection
if ($tokens[$close]->equals('}')) {
$previous = $tokens->findBlockStart(Tokens::BLOCK_TYPE_CURLY_BRACE, $close);
}
$open = $tokens->getPrevTokenOfKind($previous, [[T_IF], [T_ELSE], [T_ELSEIF]]);
if ($tokens[$open]->isGivenKind(T_IF)) {
$elseCandidate = $tokens->getPrevMeaningfulToken($open);
if ($tokens[$elseCandidate]->isGivenKind(T_ELSE)) {
$open = $elseCandidate;
}
}
return [$open, $close];
}
/**
* @param int $index Index of the token to check
* @param int $lowerLimitIndex Lower limit index. Since the token to check will always be in a conditional we must stop checking at this index
*/
private function isInConditional(Tokens $tokens, int $index, int $lowerLimitIndex): bool
{
$candidateIndex = $tokens->getPrevTokenOfKind($index, [')', ';', ':']);
if ($tokens[$candidateIndex]->equals(':')) {
return true;
}
if (!$tokens[$candidateIndex]->equals(')')) {
return false; // token is ';' or close tag
}
// token is always ')' here.
// If it is part of the condition the token is always in, return false.
// If it is not it is a nested condition so return true
$open = $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $candidateIndex);
return $tokens->getPrevMeaningfulToken($open) > $lowerLimitIndex;
}
/**
* For internal use only, as it is not perfect.
*
* Returns if the token at given index is part of an if/elseif/else statement
* without {}. Assumes not passing the last `;`/close tag of the statement, not
* out of range index, etc.
*
* @param int $index Index of the token to check
*/
private function isInConditionWithoutBraces(Tokens $tokens, int $index, int $lowerLimitIndex): bool
{
do {
if ($tokens[$index]->isComment() || $tokens[$index]->isWhitespace()) {
$index = $tokens->getPrevMeaningfulToken($index);
}
$token = $tokens[$index];
if ($token->isGivenKind([T_IF, T_ELSEIF, T_ELSE])) {
return true;
}
if ($token->equals(';')) {
return false;
}
if ($token->equals('{')) {
$index = $tokens->getPrevMeaningfulToken($index);
// OK if belongs to: for, do, while, foreach
// Not OK if belongs to: if, else, elseif
if ($tokens[$index]->isGivenKind(T_DO)) {
--$index;
continue;
}
if (!$tokens[$index]->equals(')')) {
return false; // like `else {`
}
$index = $tokens->findBlockStart(
Tokens::BLOCK_TYPE_PARENTHESIS_BRACE,
$index
);
$index = $tokens->getPrevMeaningfulToken($index);
if ($tokens[$index]->isGivenKind([T_IF, T_ELSEIF])) {
return false;
}
} elseif ($token->equals(')')) {
$type = Tokens::detectBlockType($token);
$index = $tokens->findBlockStart(
$type['type'],
$index
);
$index = $tokens->getPrevMeaningfulToken($index);
} else {
--$index;
}
} while ($index > $lowerLimitIndex);
return false;
}
}
@@ -0,0 +1,225 @@
<?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;
use PhpCsFixer\DocBlock\Annotation;
use PhpCsFixer\DocBlock\DocBlock;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
use PhpCsFixer\Tokenizer\Analyzer\NamespacesAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @internal
*/
abstract class AbstractPhpdocToTypeDeclarationFixer extends AbstractFixer implements ConfigurableFixerInterface
{
private const CLASS_REGEX = '/^\\\\?[a-zA-Z_\\x7f-\\xff](?:\\\\?[a-zA-Z0-9_\\x7f-\\xff]+)*$/';
/**
* @var array<string, int>
*/
private array $versionSpecificTypes = [
'void' => 70100,
'iterable' => 70100,
'object' => 70200,
'mixed' => 80000,
];
/**
* @var array<string, bool>
*/
private array $scalarTypes = [
'bool' => true,
'float' => true,
'int' => true,
'string' => true,
];
/**
* @var array<string, bool>
*/
private static array $syntaxValidationCache = [];
/**
* {@inheritdoc}
*/
public function isRisky(): bool
{
return true;
}
abstract protected function isSkippedType(string $type): bool;
/**
* {@inheritdoc}
*/
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('scalar_types', 'Fix also scalar types; may have unexpected behaviour due to PHP bad type coercion system.'))
->setAllowedTypes(['bool'])
->setDefault(true)
->getOption(),
]);
}
/**
* @param int $index The index of the function token
*/
protected function findFunctionDocComment(Tokens $tokens, int $index): ?int
{
do {
$index = $tokens->getPrevNonWhitespace($index);
} while ($tokens[$index]->isGivenKind([
T_COMMENT,
T_ABSTRACT,
T_FINAL,
T_PRIVATE,
T_PROTECTED,
T_PUBLIC,
T_STATIC,
]));
if ($tokens[$index]->isGivenKind(T_DOC_COMMENT)) {
return $index;
}
return null;
}
/**
* @return Annotation[]
*/
protected function getAnnotationsFromDocComment(string $name, Tokens $tokens, int $docCommentIndex): array
{
$namespacesAnalyzer = new NamespacesAnalyzer();
$namespace = $namespacesAnalyzer->getNamespaceAt($tokens, $docCommentIndex);
$namespaceUsesAnalyzer = new NamespaceUsesAnalyzer();
$namespaceUses = $namespaceUsesAnalyzer->getDeclarationsInNamespace($tokens, $namespace);
$doc = new DocBlock(
$tokens[$docCommentIndex]->getContent(),
$namespace,
$namespaceUses
);
return $doc->getAnnotationsOfType($name);
}
/**
* @return Token[]
*/
protected function createTypeDeclarationTokens(string $type, bool $isNullable): array
{
static $specialTypes = [
'array' => [CT::T_ARRAY_TYPEHINT, 'array'],
'callable' => [T_CALLABLE, 'callable'],
'static' => [T_STATIC, 'static'],
];
$newTokens = [];
if (true === $isNullable && 'mixed' !== $type) {
$newTokens[] = new Token([CT::T_NULLABLE_TYPE, '?']);
}
if (isset($specialTypes[$type])) {
$newTokens[] = new Token($specialTypes[$type]);
} else {
$typeUnqualified = ltrim($type, '\\');
if (isset($this->scalarTypes[$typeUnqualified]) || isset($this->versionSpecificTypes[$typeUnqualified])) {
// 'scalar's, 'void', 'iterable' and 'object' must be unqualified
$newTokens[] = new Token([T_STRING, $typeUnqualified]);
} else {
foreach (explode('\\', $type) as $nsIndex => $value) {
if (0 === $nsIndex && '' === $value) {
continue;
}
if (0 < $nsIndex) {
$newTokens[] = new Token([T_NS_SEPARATOR, '\\']);
}
$newTokens[] = new Token([T_STRING, $value]);
}
}
}
return $newTokens;
}
/**
* @return null|array{string, bool}
*/
protected function getCommonTypeFromAnnotation(Annotation $annotation, bool $isReturnType): ?array
{
$typesExpression = $annotation->getTypeExpression();
$commonType = $typesExpression->getCommonType();
$isNullable = $typesExpression->allowsNull();
if (null === $commonType) {
return null;
}
if ($isNullable && 'void' === $commonType) {
return null;
}
if ('static' === $commonType && (!$isReturnType || \PHP_VERSION_ID < 80000)) {
$commonType = 'self';
}
if ($this->isSkippedType($commonType)) {
return null;
}
if (isset($this->versionSpecificTypes[$commonType]) && \PHP_VERSION_ID < $this->versionSpecificTypes[$commonType]) {
return null;
}
if (isset($this->scalarTypes[$commonType])) {
if (false === $this->configuration['scalar_types']) {
return null;
}
} elseif (1 !== Preg::match(self::CLASS_REGEX, $commonType)) {
return null;
}
return [$commonType, $isNullable];
}
final protected function isValidSyntax(string $code): bool
{
if (!isset(self::$syntaxValidationCache[$code])) {
try {
Tokens::fromCode($code);
self::$syntaxValidationCache[$code] = true;
} catch (\ParseError $e) {
self::$syntaxValidationCache[$code] = false;
}
}
return self::$syntaxValidationCache[$code];
}
}
@@ -0,0 +1,128 @@
<?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;
use PhpCsFixer\DocBlock\Annotation;
use PhpCsFixer\DocBlock\DocBlock;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* This abstract fixer provides a base for fixers to fix types in PHPDoc.
*
* @author Graham Campbell <hello@gjcampbell.co.uk>
*
* @internal
*/
abstract class AbstractPhpdocTypesFixer extends AbstractFixer
{
/**
* The annotation tags search inside.
*
* @var string[]
*/
protected array $tags;
/**
* {@inheritdoc}
*/
public function __construct()
{
parent::__construct();
$this->tags = Annotation::getTagsWithTypes();
}
/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(T_DOC_COMMENT);
}
/**
* {@inheritdoc}
*/
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
foreach ($tokens as $index => $token) {
if (!$token->isGivenKind(T_DOC_COMMENT)) {
continue;
}
$doc = new DocBlock($token->getContent());
$annotations = $doc->getAnnotationsOfType($this->tags);
if (0 === \count($annotations)) {
continue;
}
foreach ($annotations as $annotation) {
$this->fixTypes($annotation);
}
$tokens[$index] = new Token([T_DOC_COMMENT, $doc->getContent()]);
}
}
/**
* Actually normalize the given type.
*/
abstract protected function normalize(string $type): string;
/**
* Fix the types at the given line.
*
* We must be super careful not to modify parts of words.
*
* This will be nicely handled behind the scenes for us by the annotation class.
*/
private function fixTypes(Annotation $annotation): void
{
$types = $annotation->getTypes();
$new = $this->normalizeTypes($types);
if ($types !== $new) {
$annotation->setTypes($new);
}
}
/**
* @param string[] $types
*
* @return string[]
*/
private function normalizeTypes(array $types): array
{
foreach ($types as $index => $type) {
$types[$index] = $this->normalizeType($type);
}
return $types;
}
/**
* Prepare the type and normalize it.
*/
private function normalizeType(string $type): string
{
return str_ends_with($type, '[]')
? $this->normalizeType(substr($type, 0, -2)).'[]'
: $this->normalize($type)
;
}
}
@@ -0,0 +1,124 @@
<?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;
use PhpCsFixer\Fixer\FixerInterface;
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
abstract class AbstractProxyFixer extends AbstractFixer
{
/**
* @var array<string, FixerInterface>
*/
protected array $proxyFixers = [];
public function __construct()
{
foreach (Utils::sortFixers($this->createProxyFixers()) as $proxyFixer) {
$this->proxyFixers[$proxyFixer->getName()] = $proxyFixer;
}
parent::__construct();
}
/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens): bool
{
foreach ($this->proxyFixers as $fixer) {
if ($fixer->isCandidate($tokens)) {
return true;
}
}
return false;
}
/**
* {@inheritdoc}
*/
public function isRisky(): bool
{
foreach ($this->proxyFixers as $fixer) {
if ($fixer->isRisky()) {
return true;
}
}
return false;
}
/**
* {@inheritdoc}
*/
public function getPriority(): int
{
if (\count($this->proxyFixers) > 1) {
throw new \LogicException('You need to override this method to provide the priority of combined fixers.');
}
return reset($this->proxyFixers)->getPriority();
}
/**
* {@inheritdoc}
*/
public function supports(\SplFileInfo $file): bool
{
foreach ($this->proxyFixers as $fixer) {
if ($fixer->supports($file)) {
return true;
}
}
return false;
}
/**
* {@inheritdoc}
*/
public function setWhitespacesConfig(WhitespacesFixerConfig $config): void
{
parent::setWhitespacesConfig($config);
foreach ($this->proxyFixers as $fixer) {
if ($fixer instanceof WhitespacesAwareFixerInterface) {
$fixer->setWhitespacesConfig($config);
}
}
}
/**
* {@inheritdoc}
*/
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
foreach ($this->proxyFixers as $fixer) {
$fixer->fix($file, $tokens);
}
}
/**
* @return FixerInterface[]
*/
abstract protected function createProxyFixers(): array;
}
@@ -0,0 +1,137 @@
<?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\Cache;
/**
* @author Andreas Möller <am@localheinz.com>
*
* @internal
*/
final class Cache implements CacheInterface
{
private SignatureInterface $signature;
/**
* @var array<string, string>
*/
private array $hashes = [];
public function __construct(SignatureInterface $signature)
{
$this->signature = $signature;
}
public function getSignature(): SignatureInterface
{
return $this->signature;
}
public function has(string $file): bool
{
return \array_key_exists($file, $this->hashes);
}
public function get(string $file): ?string
{
if (!$this->has($file)) {
return null;
}
return $this->hashes[$file];
}
public function set(string $file, string $hash): void
{
$this->hashes[$file] = $hash;
}
public function clear(string $file): void
{
unset($this->hashes[$file]);
}
public function toJson(): string
{
$json = json_encode([
'php' => $this->getSignature()->getPhpVersion(),
'version' => $this->getSignature()->getFixerVersion(),
'indent' => $this->getSignature()->getIndent(),
'lineEnding' => $this->getSignature()->getLineEnding(),
'rules' => $this->getSignature()->getRules(),
'hashes' => $this->hashes,
]);
if (JSON_ERROR_NONE !== json_last_error()) {
throw new \UnexpectedValueException(sprintf(
'Cannot encode cache signature to JSON, error: "%s". If you have non-UTF8 chars in your signature, like in license for `header_comment`, consider enabling `ext-mbstring` or install `symfony/polyfill-mbstring`.',
json_last_error_msg()
));
}
return $json;
}
/**
* @throws \InvalidArgumentException
*/
public static function fromJson(string $json): self
{
$data = json_decode($json, true);
if (null === $data && JSON_ERROR_NONE !== json_last_error()) {
throw new \InvalidArgumentException(sprintf(
'Value needs to be a valid JSON string, got "%s", error: "%s".',
$json,
json_last_error_msg()
));
}
$requiredKeys = [
'php',
'version',
'indent',
'lineEnding',
'rules',
'hashes',
];
$missingKeys = array_diff_key(array_flip($requiredKeys), $data);
if (\count($missingKeys) > 0) {
throw new \InvalidArgumentException(sprintf(
'JSON data is missing keys "%s"',
implode('", "', $missingKeys)
));
}
$signature = new Signature(
$data['php'],
$data['version'],
$data['indent'],
$data['lineEnding'],
$data['rules']
);
$cache = new self($signature);
$cache->hashes = array_map(function ($v): string {
// before v3.11.1 the hashes were crc32 encoded and saved as integers
// @TODO: remove the to string cast/array_map in v4.0
return \is_int($v) ? (string) $v : $v;
}, $data['hashes']);
return $cache;
}
}
@@ -0,0 +1,35 @@
<?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\Cache;
/**
* @author Andreas Möller <am@localheinz.com>
*
* @internal
*/
interface CacheInterface
{
public function getSignature(): SignatureInterface;
public function has(string $file): bool;
public function get(string $file): ?string;
public function set(string $file, string $hash): void;
public function clear(string $file): void;
public function toJson(): string;
}
@@ -0,0 +1,27 @@
<?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\Cache;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
interface CacheManagerInterface
{
public function needFixing(string $file, string $fileContent): bool;
public function setFile(string $file, string $fileContent): void;
}
@@ -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\Cache;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class Directory implements DirectoryInterface
{
private string $directoryName;
public function __construct(string $directoryName)
{
$this->directoryName = $directoryName;
}
/**
* {@inheritdoc}
*/
public function getRelativePathTo(string $file): string
{
$file = $this->normalizePath($file);
if (
'' === $this->directoryName
|| 0 !== stripos($file, $this->directoryName.\DIRECTORY_SEPARATOR)
) {
return $file;
}
return substr($file, \strlen($this->directoryName) + 1);
}
private function normalizePath(string $path): string
{
return str_replace(['\\', '/'], \DIRECTORY_SEPARATOR, $path);
}
}
@@ -0,0 +1,23 @@
<?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\Cache;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
interface DirectoryInterface
{
public function getRelativePathTo(string $file): string;
}
@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Cache;
/**
* Class supports caching information about state of fixing files.
*
* Cache is supported only for phar version and version installed via composer.
*
* File will be processed by PHP CS Fixer only if any of the following conditions is fulfilled:
* - cache is corrupt
* - fixer version changed
* - rules changed
* - file is new
* - file changed
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class FileCacheManager implements CacheManagerInterface
{
private FileHandlerInterface $handler;
private SignatureInterface $signature;
private bool $isDryRun;
private DirectoryInterface $cacheDirectory;
/**
* @var CacheInterface
*/
private $cache;
public function __construct(
FileHandlerInterface $handler,
SignatureInterface $signature,
bool $isDryRun = false,
?DirectoryInterface $cacheDirectory = null
) {
$this->handler = $handler;
$this->signature = $signature;
$this->isDryRun = $isDryRun;
$this->cacheDirectory = $cacheDirectory ?? new Directory('');
$this->readCache();
}
public function __destruct()
{
$this->writeCache();
}
/**
* This class is not intended to be serialized,
* and cannot be deserialized (see __wakeup method).
*/
public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
/**
* Disable the deserialization of the class to prevent attacker executing
* code by leveraging the __destruct method.
*
* @see https://owasp.org/www-community/vulnerabilities/PHP_Object_Injection
*/
public function __wakeup(): void
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function needFixing(string $file, string $fileContent): bool
{
$file = $this->cacheDirectory->getRelativePathTo($file);
return !$this->cache->has($file) || $this->cache->get($file) !== $this->calcHash($fileContent);
}
public function setFile(string $file, string $fileContent): void
{
$file = $this->cacheDirectory->getRelativePathTo($file);
$hash = $this->calcHash($fileContent);
if ($this->isDryRun && $this->cache->has($file) && $this->cache->get($file) !== $hash) {
$this->cache->clear($file);
return;
}
$this->cache->set($file, $hash);
}
private function readCache(): void
{
$cache = $this->handler->read();
if (null === $cache || !$this->signature->equals($cache->getSignature())) {
$cache = new Cache($this->signature);
}
$this->cache = $cache;
}
private function writeCache(): void
{
$this->handler->write($this->cache);
}
private function calcHash(string $content): string
{
return md5($content);
}
}
@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Cache;
use Symfony\Component\Filesystem\Exception\IOException;
/**
* @author Andreas Möller <am@localheinz.com>
*
* @internal
*/
final class FileHandler implements FileHandlerInterface
{
private string $file;
public function __construct(string $file)
{
$this->file = $file;
}
public function getFile(): string
{
return $this->file;
}
public function read(): ?CacheInterface
{
if (!file_exists($this->file)) {
return null;
}
$content = file_get_contents($this->file);
try {
$cache = Cache::fromJson($content);
} catch (\InvalidArgumentException $exception) {
return null;
}
return $cache;
}
public function write(CacheInterface $cache): void
{
$content = $cache->toJson();
if (file_exists($this->file)) {
if (is_dir($this->file)) {
throw new IOException(
sprintf('Cannot write cache file "%s" as the location exists as directory.', realpath($this->file)),
0,
null,
$this->file
);
}
if (!is_writable($this->file)) {
throw new IOException(
sprintf('Cannot write to file "%s" as it is not writable.', realpath($this->file)),
0,
null,
$this->file
);
}
} else {
$dir = \dirname($this->file);
if (!is_dir($dir)) {
throw new IOException(
sprintf('Directory of cache file "%s" does not exists.', $this->file),
0,
null,
$this->file
);
}
@touch($this->file);
@chmod($this->file, 0666);
}
$bytesWritten = @file_put_contents($this->file, $content);
if (false === $bytesWritten) {
$error = error_get_last();
throw new IOException(
sprintf('Failed to write file "%s", "%s".', $this->file, $error['message'] ?? 'no reason available'),
0,
null,
$this->file
);
}
}
}
@@ -0,0 +1,29 @@
<?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\Cache;
/**
* @author Andreas Möller <am@localheinz.com>
*
* @internal
*/
interface FileHandlerInterface
{
public function getFile(): string;
public function read(): ?CacheInterface;
public function write(CacheInterface $cache): void;
}
@@ -0,0 +1,32 @@
<?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\Cache;
/**
* @author Andreas Möller <am@localheinz.com>
*
* @internal
*/
final class NullCacheManager implements CacheManagerInterface
{
public function needFixing(string $file, string $fileContent): bool
{
return true;
}
public function setFile(string $file, string $fileContent): void
{
}
}
@@ -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\Cache;
/**
* @author Andreas Möller <am@localheinz.com>
*
* @internal
*/
final class Signature implements SignatureInterface
{
private string $phpVersion;
private string $fixerVersion;
private string $indent;
private string $lineEnding;
/**
* @var array<string, array<string, mixed>|bool>
*/
private array $rules;
/**
* @param array<string, array<string, mixed>|bool> $rules
*/
public function __construct(string $phpVersion, string $fixerVersion, string $indent, string $lineEnding, array $rules)
{
$this->phpVersion = $phpVersion;
$this->fixerVersion = $fixerVersion;
$this->indent = $indent;
$this->lineEnding = $lineEnding;
$this->rules = self::makeJsonEncodable($rules);
}
public function getPhpVersion(): string
{
return $this->phpVersion;
}
public function getFixerVersion(): string
{
return $this->fixerVersion;
}
public function getIndent(): string
{
return $this->indent;
}
public function getLineEnding(): string
{
return $this->lineEnding;
}
public function getRules(): array
{
return $this->rules;
}
public function equals(SignatureInterface $signature): bool
{
return $this->phpVersion === $signature->getPhpVersion()
&& $this->fixerVersion === $signature->getFixerVersion()
&& $this->indent === $signature->getIndent()
&& $this->lineEnding === $signature->getLineEnding()
&& $this->rules === $signature->getRules();
}
/**
* @param array<string, array<string, mixed>|bool> $data
*
* @return array<string, array<string, mixed>|bool>
*/
private static function makeJsonEncodable(array $data): array
{
array_walk_recursive($data, static function (&$item): void {
if (\is_string($item) && !mb_detect_encoding($item, 'utf-8', true)) {
$item = base64_encode($item);
}
});
return $data;
}
}
@@ -0,0 +1,38 @@
<?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\Cache;
/**
* @author Andreas Möller <am@localheinz.com>
*
* @internal
*/
interface SignatureInterface
{
public function getPhpVersion(): string;
public function getFixerVersion(): string;
public function getIndent(): string;
public function getLineEnding(): string;
/**
* @return array<string, array<string, mixed>|bool>
*/
public function getRules(): array;
public function equals(self $signature): bool;
}
+285
View File
@@ -0,0 +1,285 @@
<?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;
use PhpCsFixer\Fixer\FixerInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Katsuhiro Ogawa <ko.fivestar@gmail.com>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
class Config implements ConfigInterface
{
private string $cacheFile = '.php-cs-fixer.cache';
/**
* @var FixerInterface[]
*/
private array $customFixers = [];
/**
* @var null|iterable<\SplFileInfo>
*/
private ?iterable $finder = null;
private string $format = 'txt';
private bool $hideProgress = false;
private string $indent = ' ';
private bool $isRiskyAllowed = false;
private string $lineEnding = "\n";
private string $name;
/**
* @var null|string
*/
private $phpExecutable;
/**
* @TODO: 4.0 - update to @PER
*
* @var array<string, array<string, mixed>|bool>
*/
private array $rules = ['@PSR12' => true];
private bool $usingCache = true;
public function __construct(string $name = 'default')
{
$this->name = $name;
}
/**
* {@inheritdoc}
*/
public function getCacheFile(): string
{
return $this->cacheFile;
}
/**
* {@inheritdoc}
*/
public function getCustomFixers(): array
{
return $this->customFixers;
}
/**
* @return Finder
*/
public function getFinder(): iterable
{
if (null === $this->finder) {
$this->finder = new Finder();
}
return $this->finder;
}
/**
* {@inheritdoc}
*/
public function getFormat(): string
{
return $this->format;
}
/**
* {@inheritdoc}
*/
public function getHideProgress(): bool
{
return $this->hideProgress;
}
/**
* {@inheritdoc}
*/
public function getIndent(): string
{
return $this->indent;
}
/**
* {@inheritdoc}
*/
public function getLineEnding(): string
{
return $this->lineEnding;
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
return $this->name;
}
/**
* {@inheritdoc}
*/
public function getPhpExecutable(): ?string
{
return $this->phpExecutable;
}
/**
* {@inheritdoc}
*/
public function getRiskyAllowed(): bool
{
return $this->isRiskyAllowed;
}
/**
* {@inheritdoc}
*/
public function getRules(): array
{
return $this->rules;
}
/**
* {@inheritdoc}
*/
public function getUsingCache(): bool
{
return $this->usingCache;
}
/**
* {@inheritdoc}
*/
public function registerCustomFixers(iterable $fixers): ConfigInterface
{
foreach ($fixers as $fixer) {
$this->addCustomFixer($fixer);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function setCacheFile(string $cacheFile): ConfigInterface
{
$this->cacheFile = $cacheFile;
return $this;
}
/**
* {@inheritdoc}
*/
public function setFinder(iterable $finder): ConfigInterface
{
$this->finder = $finder;
return $this;
}
/**
* {@inheritdoc}
*/
public function setFormat(string $format): ConfigInterface
{
$this->format = $format;
return $this;
}
/**
* {@inheritdoc}
*/
public function setHideProgress(bool $hideProgress): ConfigInterface
{
$this->hideProgress = $hideProgress;
return $this;
}
/**
* {@inheritdoc}
*/
public function setIndent(string $indent): ConfigInterface
{
$this->indent = $indent;
return $this;
}
/**
* {@inheritdoc}
*/
public function setLineEnding(string $lineEnding): ConfigInterface
{
$this->lineEnding = $lineEnding;
return $this;
}
/**
* {@inheritdoc}
*/
public function setPhpExecutable(?string $phpExecutable): ConfigInterface
{
$this->phpExecutable = $phpExecutable;
return $this;
}
/**
* {@inheritdoc}
*/
public function setRiskyAllowed(bool $isRiskyAllowed): ConfigInterface
{
$this->isRiskyAllowed = $isRiskyAllowed;
return $this;
}
/**
* {@inheritdoc}
*/
public function setRules(array $rules): ConfigInterface
{
$this->rules = $rules;
return $this;
}
/**
* {@inheritdoc}
*/
public function setUsingCache(bool $usingCache): ConfigInterface
{
$this->usingCache = $usingCache;
return $this;
}
private function addCustomFixer(FixerInterface $fixer): void
{
$this->customFixers[] = $fixer;
}
}
@@ -0,0 +1,140 @@
<?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;
use PhpCsFixer\Fixer\FixerInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
interface ConfigInterface
{
/**
* Returns the path to the cache file.
*
* @return null|string Returns null if not using cache
*/
public function getCacheFile(): ?string;
/**
* Returns the custom fixers to use.
*
* @return FixerInterface[]
*/
public function getCustomFixers(): array;
/**
* Returns files to scan.
*
* @return iterable<\SplFileInfo>
*/
public function getFinder(): iterable;
public function getFormat(): string;
/**
* Returns true if progress should be hidden.
*/
public function getHideProgress(): bool;
public function getIndent(): string;
public function getLineEnding(): string;
/**
* Returns the name of the configuration.
*
* The name must be all lowercase and without any spaces.
*
* @return string The name of the configuration
*/
public function getName(): string;
/**
* Get configured PHP executable, if any.
*/
public function getPhpExecutable(): ?string;
/**
* Check if it is allowed to run risky fixers.
*/
public function getRiskyAllowed(): bool;
/**
* Get rules.
*
* Keys of array are names of fixers/sets, values are true/false.
*
* @return array<string, array<string, mixed>|bool>
*/
public function getRules(): array;
/**
* Returns true if caching should be enabled.
*/
public function getUsingCache(): bool;
/**
* Adds a suite of custom fixers.
*
* Name of custom fixer should follow `VendorName/rule_name` convention.
*
* @param FixerInterface[]|iterable|\Traversable $fixers
*/
public function registerCustomFixers(iterable $fixers): self;
/**
* Sets the path to the cache file.
*/
public function setCacheFile(string $cacheFile): self;
/**
* @param iterable<\SplFileInfo> $finder
*/
public function setFinder(iterable $finder): self;
public function setFormat(string $format): self;
public function setHideProgress(bool $hideProgress): self;
public function setIndent(string $indent): self;
public function setLineEnding(string $lineEnding): self;
/**
* Set PHP executable.
*/
public function setPhpExecutable(?string $phpExecutable): self;
/**
* Set if it is allowed to run risky fixers.
*/
public function setRiskyAllowed(bool $isRiskyAllowed): self;
/**
* Set rules.
*
* Keys of array are names of fixers or sets.
* Value for set must be bool (turn it on or off).
* Value for fixer may be bool (turn it on or off) or array of configuration
* (turn it on and contains configuration for FixerInterface::configure method).
*
* @param array<string, array<string, mixed>|bool> $rules
*/
public function setRules(array $rules): self;
public function setUsingCache(bool $usingCache): self;
}
@@ -0,0 +1,36 @@
<?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\ConfigurationException;
use PhpCsFixer\Console\Command\FixCommandExitStatusCalculator;
/**
* Exceptions of this type are thrown on misconfiguration of the Fixer.
*
* @internal
*
* @final Only internal extending this class is supported
*/
class InvalidConfigurationException extends \InvalidArgumentException
{
public function __construct(string $message, ?int $code = null, ?\Throwable $previous = null)
{
parent::__construct(
$message,
$code ?? FixCommandExitStatusCalculator::EXIT_STATUS_FLAG_HAS_INVALID_CONFIG,
$previous
);
}
}
@@ -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\ConfigurationException;
use PhpCsFixer\Console\Command\FixCommandExitStatusCalculator;
/**
* Exception thrown by Fixers on misconfiguration.
*
* @internal
*
* @final Only internal extending this class is supported
*/
class InvalidFixerConfigurationException extends InvalidConfigurationException
{
private string $fixerName;
public function __construct(string $fixerName, string $message, ?\Throwable $previous = null)
{
parent::__construct(
sprintf('[%s] %s', $fixerName, $message),
FixCommandExitStatusCalculator::EXIT_STATUS_FLAG_HAS_INVALID_FIXER_CONFIG,
$previous
);
$this->fixerName = $fixerName;
}
public function getFixerName(): string
{
return $this->fixerName;
}
}
@@ -0,0 +1,24 @@
<?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\ConfigurationException;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class InvalidForEnvFixerConfigurationException extends InvalidFixerConfigurationException
{
}
@@ -0,0 +1,24 @@
<?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\ConfigurationException;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class RequiredFixerConfigurationException extends InvalidFixerConfigurationException
{
}
@@ -0,0 +1,142 @@
<?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\Console;
use PhpCsFixer\Console\Command\DescribeCommand;
use PhpCsFixer\Console\Command\FixCommand;
use PhpCsFixer\Console\Command\HelpCommand;
use PhpCsFixer\Console\Command\ListFilesCommand;
use PhpCsFixer\Console\Command\ListSetsCommand;
use PhpCsFixer\Console\Command\SelfUpdateCommand;
use PhpCsFixer\Console\SelfUpdate\GithubClient;
use PhpCsFixer\Console\SelfUpdate\NewVersionChecker;
use PhpCsFixer\PharChecker;
use PhpCsFixer\ToolInfo;
use PhpCsFixer\Utils;
use Symfony\Component\Console\Application as BaseApplication;
use Symfony\Component\Console\Command\ListCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class Application extends BaseApplication
{
public const VERSION = '3.13.0';
public const VERSION_CODENAME = 'Oliva';
private ToolInfo $toolInfo;
public function __construct()
{
parent::__construct('PHP CS Fixer', self::VERSION);
$this->toolInfo = new ToolInfo();
// in alphabetical order
$this->add(new DescribeCommand());
$this->add(new FixCommand($this->toolInfo));
$this->add(new ListFilesCommand($this->toolInfo));
$this->add(new ListSetsCommand());
$this->add(new SelfUpdateCommand(
new NewVersionChecker(new GithubClient()),
$this->toolInfo,
new PharChecker()
));
}
public static function getMajorVersion(): int
{
return (int) explode('.', self::VERSION)[0];
}
/**
* {@inheritdoc}
*/
public function doRun(InputInterface $input, OutputInterface $output): int
{
$stdErr = $output instanceof ConsoleOutputInterface
? $output->getErrorOutput()
: ($input->hasParameterOption('--format', true) && 'txt' !== $input->getParameterOption('--format', null, true) ? null : $output)
;
if (null !== $stdErr) {
$warningsDetector = new WarningsDetector($this->toolInfo);
$warningsDetector->detectOldVendor();
$warningsDetector->detectOldMajor();
$warnings = $warningsDetector->getWarnings();
if (\count($warnings) > 0) {
foreach ($warnings as $warning) {
$stdErr->writeln(sprintf($stdErr->isDecorated() ? '<bg=yellow;fg=black;>%s</>' : '%s', $warning));
}
$stdErr->writeln('');
}
}
$result = parent::doRun($input, $output);
if (
null !== $stdErr
&& $output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE
) {
$triggeredDeprecations = Utils::getTriggeredDeprecations();
if (\count($triggeredDeprecations) > 0) {
$stdErr->writeln('');
$stdErr->writeln($stdErr->isDecorated() ? '<bg=yellow;fg=black;>Detected deprecations in use:</>' : 'Detected deprecations in use:');
foreach ($triggeredDeprecations as $deprecation) {
$stdErr->writeln(sprintf('- %s', $deprecation));
}
}
}
return $result;
}
/**
* {@inheritdoc}
*/
public function getLongVersion(): string
{
$commit = '@git-commit@';
$versionCommit = '';
if ('@'.'git-commit@' !== $commit) { /** @phpstan-ignore-line as `$commit` is replaced during phar building */
$versionCommit = substr($commit, 0, 7);
}
return implode('', [
parent::getLongVersion(),
$versionCommit ? sprintf(' <info>(%s)</info>', $versionCommit) : '', // @phpstan-ignore-line to avoid `Ternary operator condition is always true|false.`
self::VERSION_CODENAME ? sprintf(' <info>%s</info>', self::VERSION_CODENAME) : '', // @phpstan-ignore-line to avoid `Ternary operator condition is always true|false.`
' by <comment>Fabien Potencier</comment> and <comment>Dariusz Ruminski</comment>.',
"\nPHP runtime: <info>".PHP_VERSION.'</info>',
]);
}
/**
* {@inheritdoc}
*/
protected function getDefaultCommands(): array
{
return [new HelpCommand(), new ListCommand()];
}
}
@@ -0,0 +1,428 @@
<?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\Console\Command;
use PhpCsFixer\Differ\DiffConsoleFormatter;
use PhpCsFixer\Differ\FullDiffer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\DeprecatedFixerInterface;
use PhpCsFixer\Fixer\FixerInterface;
use PhpCsFixer\FixerConfiguration\AliasedFixerOption;
use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
use PhpCsFixer\FixerConfiguration\DeprecatedFixerOption;
use PhpCsFixer\FixerDefinition\CodeSampleInterface;
use PhpCsFixer\FixerDefinition\FileSpecificCodeSampleInterface;
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSampleInterface;
use PhpCsFixer\FixerFactory;
use PhpCsFixer\Preg;
use PhpCsFixer\RuleSet\RuleSets;
use PhpCsFixer\StdinFileInfo;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Utils;
use PhpCsFixer\WordMatcher;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
#[AsCommand(name: 'describe')]
final class DescribeCommand extends Command
{
/**
* @var string
*/
protected static $defaultName = 'describe';
/**
* @var string[]
*/
private $setNames;
private FixerFactory $fixerFactory;
/**
* @var array<string, FixerInterface>
*/
private $fixers;
public function __construct(?FixerFactory $fixerFactory = null)
{
parent::__construct();
if (null === $fixerFactory) {
$fixerFactory = new FixerFactory();
$fixerFactory->registerBuiltInFixers();
}
$this->fixerFactory = $fixerFactory;
}
/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this
->setDefinition(
[
new InputArgument('name', InputArgument::REQUIRED, 'Name of rule / set.'),
]
)
->setDescription('Describe rule / ruleset.')
;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity() && $output instanceof ConsoleOutputInterface) {
$stdErr = $output->getErrorOutput();
$stdErr->writeln($this->getApplication()->getLongVersion());
}
$name = $input->getArgument('name');
try {
if (str_starts_with($name, '@')) {
$this->describeSet($output, $name);
return 0;
}
$this->describeRule($output, $name);
} catch (DescribeNameNotFoundException $e) {
$matcher = new WordMatcher(
'set' === $e->getType() ? $this->getSetNames() : array_keys($this->getFixers())
);
$alternative = $matcher->match($name);
$this->describeList($output, $e->getType());
throw new \InvalidArgumentException(sprintf(
'%s "%s" not found.%s',
ucfirst($e->getType()),
$name,
null === $alternative ? '' : ' Did you mean "'.$alternative.'"?'
));
}
return 0;
}
private function describeRule(OutputInterface $output, string $name): void
{
$fixers = $this->getFixers();
if (!isset($fixers[$name])) {
throw new DescribeNameNotFoundException($name, 'rule');
}
/** @var FixerInterface $fixer */
$fixer = $fixers[$name];
$definition = $fixer->getDefinition();
$summary = $definition->getSummary();
if ($fixer instanceof DeprecatedFixerInterface) {
$successors = $fixer->getSuccessorsNames();
$message = [] === $successors
? 'will be removed on next major version'
: sprintf('use %s instead', Utils::naturalLanguageJoinWithBackticks($successors));
$message = Preg::replace('/(`.+?`)/', '<info>$1</info>', $message);
$summary .= sprintf(' <error>DEPRECATED</error>: %s.', $message);
}
$output->writeln(sprintf('<info>Description of</info> %s <info>rule</info>.', $name));
if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
$output->writeln(sprintf('Fixer class: <comment>%s</comment>.', \get_class($fixer)));
}
$output->writeln($summary);
$description = $definition->getDescription();
if (null !== $description) {
$output->writeln($description);
}
$output->writeln('');
if ($fixer->isRisky()) {
$output->writeln('<error>Fixer applying this rule is risky.</error>');
$riskyDescription = $definition->getRiskyDescription();
if (null !== $riskyDescription) {
$output->writeln($riskyDescription);
}
$output->writeln('');
}
if ($fixer instanceof ConfigurableFixerInterface) {
$configurationDefinition = $fixer->getConfigurationDefinition();
$options = $configurationDefinition->getOptions();
$output->writeln(sprintf('Fixer is configurable using following option%s:', 1 === \count($options) ? '' : 's'));
foreach ($options as $option) {
$line = '* <info>'.OutputFormatter::escape($option->getName()).'</info>';
$allowed = HelpCommand::getDisplayableAllowedValues($option);
if (null === $allowed) {
$allowed = array_map(
static fn (string $type): string => '<comment>'.$type.'</comment>',
$option->getAllowedTypes(),
);
} else {
$allowed = array_map(static function ($value): string {
return $value instanceof AllowedValueSubset
? 'a subset of <comment>'.HelpCommand::toString($value->getAllowedValues()).'</comment>'
: '<comment>'.HelpCommand::toString($value).'</comment>';
}, $allowed);
}
$line .= ' ('.implode(', ', $allowed).')';
$description = Preg::replace('/(`.+?`)/', '<info>$1</info>', OutputFormatter::escape($option->getDescription()));
$line .= ': '.lcfirst(Preg::replace('/\.$/', '', $description)).'; ';
if ($option->hasDefault()) {
$line .= sprintf(
'defaults to <comment>%s</comment>',
HelpCommand::toString($option->getDefault())
);
} else {
$line .= '<comment>required</comment>';
}
if ($option instanceof DeprecatedFixerOption) {
$line .= '. <error>DEPRECATED</error>: '.Preg::replace(
'/(`.+?`)/',
'<info>$1</info>',
OutputFormatter::escape(lcfirst($option->getDeprecationMessage()))
);
}
if ($option instanceof AliasedFixerOption) {
$line .= '; <error>DEPRECATED</error> alias: <comment>'.$option->getAlias().'</comment>';
}
$output->writeln($line);
}
$output->writeln('');
}
/** @var CodeSampleInterface[] $codeSamples */
$codeSamples = array_filter($definition->getCodeSamples(), static function (CodeSampleInterface $codeSample): bool {
if ($codeSample instanceof VersionSpecificCodeSampleInterface) {
return $codeSample->isSuitableFor(\PHP_VERSION_ID);
}
return true;
});
if (0 === \count($codeSamples)) {
$output->writeln([
'Fixing examples cannot be demonstrated on the current PHP version.',
'',
]);
} else {
$output->writeln('Fixing examples:');
$differ = new FullDiffer();
$diffFormatter = new DiffConsoleFormatter(
$output->isDecorated(),
sprintf(
'<comment> ---------- begin diff ----------</comment>%s%%s%s<comment> ----------- end diff -----------</comment>',
PHP_EOL,
PHP_EOL
)
);
foreach ($codeSamples as $index => $codeSample) {
$old = $codeSample->getCode();
$tokens = Tokens::fromCode($old);
$configuration = $codeSample->getConfiguration();
if ($fixer instanceof ConfigurableFixerInterface) {
$fixer->configure($configuration ?? []);
}
$file = $codeSample instanceof FileSpecificCodeSampleInterface
? $codeSample->getSplFileInfo()
: new StdinFileInfo();
$fixer->fix($file, $tokens);
$diff = $differ->diff($old, $tokens->generateCode());
if ($fixer instanceof ConfigurableFixerInterface) {
if (null === $configuration) {
$output->writeln(sprintf(' * Example #%d. Fixing with the <comment>default</comment> configuration.', $index + 1));
} else {
$output->writeln(sprintf(' * Example #%d. Fixing with configuration: <comment>%s</comment>.', $index + 1, HelpCommand::toString($codeSample->getConfiguration())));
}
} else {
$output->writeln(sprintf(' * Example #%d.', $index + 1));
}
$output->writeln([$diffFormatter->format($diff, ' %s'), '']);
}
}
}
private function describeSet(OutputInterface $output, string $name): void
{
if (!\in_array($name, $this->getSetNames(), true)) {
throw new DescribeNameNotFoundException($name, 'set');
}
$ruleSetDefinitions = RuleSets::getSetDefinitions();
$fixers = $this->getFixers();
$output->writeln(sprintf('<info>Description of the</info> %s <info>set.</info>', $ruleSetDefinitions[$name]->getName()));
$output->writeln($this->replaceRstLinks($ruleSetDefinitions[$name]->getDescription()));
if ($ruleSetDefinitions[$name]->isRisky()) {
$output->writeln('This set contains <error>risky</error> rules.');
}
$output->writeln('');
$help = '';
foreach ($ruleSetDefinitions[$name]->getRules() as $rule => $config) {
if (str_starts_with($rule, '@')) {
$set = $ruleSetDefinitions[$rule];
$help .= sprintf(
" * <info>%s</info>%s\n | %s\n\n",
$rule,
$set->isRisky() ? ' <error>risky</error>' : '',
$this->replaceRstLinks($set->getDescription())
);
continue;
}
/** @var FixerInterface $fixer */
$fixer = $fixers[$rule];
$definition = $fixer->getDefinition();
$help .= sprintf(
" * <info>%s</info>%s\n | %s\n%s\n",
$rule,
$fixer->isRisky() ? ' <error>risky</error>' : '',
$definition->getSummary(),
true !== $config ? sprintf(" <comment>| Configuration: %s</comment>\n", HelpCommand::toString($config)) : ''
);
}
$output->write($help);
}
/**
* @return array<string, FixerInterface>
*/
private function getFixers(): array
{
if (null !== $this->fixers) {
return $this->fixers;
}
$fixers = [];
foreach ($this->fixerFactory->getFixers() as $fixer) {
$fixers[$fixer->getName()] = $fixer;
}
$this->fixers = $fixers;
ksort($this->fixers);
return $this->fixers;
}
/**
* @return string[]
*/
private function getSetNames(): array
{
if (null !== $this->setNames) {
return $this->setNames;
}
$this->setNames = RuleSets::getSetDefinitionNames();
return $this->setNames;
}
/**
* @param string $type 'rule'|'set'
*/
private function describeList(OutputInterface $output, string $type): void
{
if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE) {
$describe = [
'sets' => $this->getSetNames(),
'rules' => $this->getFixers(),
];
} elseif ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
$describe = 'set' === $type ? ['sets' => $this->getSetNames()] : ['rules' => $this->getFixers()];
} else {
return;
}
/** @var string[] $items */
foreach ($describe as $list => $items) {
$output->writeln(sprintf('<comment>Defined %s:</comment>', $list));
foreach ($items as $name => $item) {
$output->writeln(sprintf('* <info>%s</info>', \is_string($name) ? $name : $item));
}
}
}
private function replaceRstLinks(string $content): string
{
return Preg::replaceCallback(
'/(`[^<]+<[^>]+>`_)/',
static function (array $matches) {
return Preg::replaceCallback(
'/`(.*)<(.*)>`_/',
static function (array $matches): string {
return $matches[1].'('.$matches[2].')';
},
$matches[1]
);
},
$content
);
}
}
@@ -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\Console\Command;
/**
* @internal
*/
final class DescribeNameNotFoundException extends \InvalidArgumentException
{
private string $name;
/**
* 'rule'|'set'.
*/
private string $type;
public function __construct(string $name, string $type)
{
$this->name = $name;
$this->type = $type;
parent::__construct();
}
public function getName(): string
{
return $this->name;
}
public function getType(): string
{
return $this->type;
}
}
@@ -0,0 +1,128 @@
<?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\Console\Command;
use PhpCsFixer\Documentation\DocumentationLocator;
use PhpCsFixer\Documentation\FixerDocumentGenerator;
use PhpCsFixer\Documentation\ListDocumentGenerator;
use PhpCsFixer\Documentation\RuleSetDocumentationGenerator;
use PhpCsFixer\FixerFactory;
use PhpCsFixer\RuleSet\RuleSets;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
/**
* @internal
*/
#[AsCommand(name: 'documentation')]
final class DocumentationCommand extends Command
{
/**
* @var string
*/
protected static $defaultName = 'documentation';
protected function configure(): void
{
$this
->setAliases(['doc'])
->setDescription('Dumps the documentation of the project into its "/doc" directory.')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$filesystem = new Filesystem();
$locator = new DocumentationLocator();
$fixerFactory = new FixerFactory();
$fixerFactory->registerBuiltInFixers();
$fixers = $fixerFactory->getFixers();
$setDefinitions = RuleSets::getSetDefinitions();
$fixerDocumentGenerator = new FixerDocumentGenerator($locator);
$ruleSetDocumentationGenerator = new RuleSetDocumentationGenerator($locator);
$listDocumentGenerator = new ListDocumentGenerator($locator);
// Array of existing fixer docs.
// We first override existing files, and then we will delete files that are no longer needed.
// We cannot remove all files first, as generation of docs is re-using existing docs to extract code-samples for
// VersionSpecificCodeSample under incompatible PHP version.
$docForFixerRelativePaths = [];
foreach ($fixers as $fixer) {
$docForFixerRelativePaths[] = $locator->getFixerDocumentationFileRelativePath($fixer);
$filesystem->dumpFile(
$locator->getFixerDocumentationFilePath($fixer),
$fixerDocumentGenerator->generateFixerDocumentation($fixer)
);
}
/** @var SplFileInfo $file */
foreach (
(new Finder())->files()
->in($locator->getFixersDocumentationDirectoryPath())
->notPath($docForFixerRelativePaths) as $file
) {
$filesystem->remove($file->getPathname());
}
// Fixer doc. index
$filesystem->dumpFile(
$locator->getFixersDocumentationIndexFilePath(),
$fixerDocumentGenerator->generateFixersDocumentationIndex($fixers)
);
// RuleSet docs.
/** @var SplFileInfo $file */
foreach ((new Finder())->files()->in($locator->getRuleSetsDocumentationDirectoryPath()) as $file) {
$filesystem->remove($file->getPathname());
}
$paths = [];
foreach ($setDefinitions as $name => $definition) {
$path = $locator->getRuleSetsDocumentationFilePath($name);
$paths[$name] = $path;
$filesystem->dumpFile($path, $ruleSetDocumentationGenerator->generateRuleSetsDocumentation($definition, $fixers));
}
// RuleSet doc. index
$filesystem->dumpFile(
$locator->getRuleSetsDocumentationIndexFilePath(),
$ruleSetDocumentationGenerator->generateRuleSetsDocumentationIndex($paths)
);
// List file / Appendix
$filesystem->dumpFile(
$locator->getListingFilePath(),
$listDocumentGenerator->generateListingDocumentation($fixers)
);
$output->writeln('Docs updated.');
return 0;
}
}
@@ -0,0 +1,360 @@
<?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\Console\Command;
use PhpCsFixer\Config;
use PhpCsFixer\ConfigInterface;
use PhpCsFixer\ConfigurationException\InvalidConfigurationException;
use PhpCsFixer\Console\ConfigurationResolver;
use PhpCsFixer\Console\Output\ErrorOutput;
use PhpCsFixer\Console\Output\NullOutput;
use PhpCsFixer\Console\Output\ProcessOutput;
use PhpCsFixer\Console\Report\FixReport\ReportSummary;
use PhpCsFixer\Error\ErrorsManager;
use PhpCsFixer\Runner\Runner;
use PhpCsFixer\ToolInfoInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Terminal;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Stopwatch\Stopwatch;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
#[AsCommand(name: 'fix')]
final class FixCommand extends Command
{
/**
* @var string
*/
protected static $defaultName = 'fix';
private EventDispatcherInterface $eventDispatcher;
private ErrorsManager $errorsManager;
private Stopwatch $stopwatch;
private ConfigInterface $defaultConfig;
private ToolInfoInterface $toolInfo;
public function __construct(ToolInfoInterface $toolInfo)
{
parent::__construct();
$this->eventDispatcher = new EventDispatcher();
$this->errorsManager = new ErrorsManager();
$this->stopwatch = new Stopwatch();
$this->defaultConfig = new Config();
$this->toolInfo = $toolInfo;
}
/**
* {@inheritdoc}
*
* Override here to only generate the help copy when used.
*/
public function getHelp(): string
{
return <<<'EOF'
The <info>%command.name%</info> command tries to fix as much coding standards
problems as possible on a given file or files in a given directory and its subdirectories:
<info>$ php %command.full_name% /path/to/dir</info>
<info>$ php %command.full_name% /path/to/file</info>
By default <comment>--path-mode</comment> is set to `override`, which means, that if you specify the path to a file or a directory via
command arguments, then the paths provided to a `Finder` in config file will be ignored. You can use <comment>--path-mode=intersection</comment>
to merge paths from the config file and from the argument:
<info>$ php %command.full_name% --path-mode=intersection /path/to/dir</info>
The <comment>--format</comment> option for the output format. Supported formats are `txt` (default one), `json`, `xml`, `checkstyle`, `junit` and `gitlab`.
NOTE: the output for the following formats are generated in accordance with schemas
* `checkstyle` follows the common `"checkstyle" XML schema </doc/schemas/fix/checkstyle.xsd>`_
* `json` follows the `own JSON schema </doc/schemas/fix/schema.json>`_
* `junit` follows the `JUnit XML schema from Jenkins </doc/schemas/fix/junit-10.xsd>`_
* `xml` follows the `own XML schema </doc/schemas/fix/xml.xsd>`_
The <comment>--quiet</comment> Do not output any message.
The <comment>--verbose</comment> option will show the applied rules. When using the `txt` format it will also display progress notifications.
NOTE: if there is an error like "errors reported during linting after fixing", you can use this to be even more verbose for debugging purpose
* `-v`: verbose
* `-vv`: very verbose
* `-vvv`: debug
The <comment>--rules</comment> option limits the rules to apply to the
project:
EOF. /* @TODO: 4.0 - change to @PER */ <<<'EOF'
<info>$ php %command.full_name% /path/to/project --rules=@PSR12</info>
By default the PSR-12 rules are used.
The <comment>--rules</comment> option lets you choose the exact rules to
apply (the rule names must be separated by a comma):
<info>$ php %command.full_name% /path/to/dir --rules=line_ending,full_opening_tag,indentation_type</info>
You can also exclude the rules you don't want by placing a dash in front of the rule name, if this is more convenient,
using <comment>-name_of_fixer</comment>:
<info>$ php %command.full_name% /path/to/dir --rules=-full_opening_tag,-indentation_type</info>
When using combinations of exact and exclude rules, applying exact rules along with above excluded results:
<info>$ php %command.full_name% /path/to/project --rules=@Symfony,-@PSR1,-blank_line_before_statement,strict_comparison</info>
Complete configuration for rules can be supplied using a `json` formatted string.
<info>$ php %command.full_name% /path/to/project --rules='{"concat_space": {"spacing": "none"}}'</info>
The <comment>--dry-run</comment> flag will run the fixer without making changes to your files.
The <comment>--diff</comment> flag can be used to let the fixer output all the changes it makes.
The <comment>--allow-risky</comment> option (pass `yes` or `no`) allows you to set whether risky rules may run. Default value is taken from config file.
A rule is considered risky if it could change code behaviour. By default no risky rules are run.
The <comment>--stop-on-violation</comment> flag stops the execution upon first file that needs to be fixed.
The <comment>--show-progress</comment> option allows you to choose the way process progress is rendered:
* <comment>none</comment>: disables progress output;
* <comment>dots</comment>: multiline progress output with number of files and percentage on each line.
If the option is not provided, it defaults to <comment>dots</comment> unless a config file that disables output is used, in which case it defaults to <comment>none</comment>. This option has no effect if the verbosity of the command is less than <comment>verbose</comment>.
<info>$ php %command.full_name% --verbose --show-progress=dots</info>
By using <command>--using-cache</command> option with `yes` or `no` you can set if the caching
mechanism should be used.
The command can also read from standard input, in which case it won't
automatically fix anything:
<info>$ cat foo.php | php %command.full_name% --diff -</info>
Finally, if you don't need BC kept on CLI level, you might use `PHP_CS_FIXER_FUTURE_MODE` to start using options that
would be default in next MAJOR release and to forbid using deprecated configuration:
<info>$ PHP_CS_FIXER_FUTURE_MODE=1 php %command.full_name% -v --diff</info>
Exit code
---------
Exit code of the fix command is built using following bit flags:
* 0 - OK.
* 1 - General error (or PHP minimal requirement not matched).
* 4 - Some files have invalid syntax (only in dry-run mode).
* 8 - Some files need fixing (only in dry-run mode).
* 16 - Configuration error of the application.
* 32 - Configuration error of a Fixer.
* 64 - Exception raised within the application.
EOF
;
}
/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this
->setDefinition(
[
new InputArgument('path', InputArgument::IS_ARRAY, 'The path.'),
new InputOption('path-mode', '', InputOption::VALUE_REQUIRED, 'Specify path mode (can be override or intersection).', ConfigurationResolver::PATH_MODE_OVERRIDE),
new InputOption('allow-risky', '', InputOption::VALUE_REQUIRED, 'Are risky fixers allowed (can be yes or no).'),
new InputOption('config', '', InputOption::VALUE_REQUIRED, 'The path to a .php-cs-fixer.php file.'),
new InputOption('dry-run', '', InputOption::VALUE_NONE, 'Only shows which files would have been modified.'),
new InputOption('rules', '', InputOption::VALUE_REQUIRED, 'The rules.'),
new InputOption('using-cache', '', InputOption::VALUE_REQUIRED, 'Does cache should be used (can be yes or no).'),
new InputOption('cache-file', '', InputOption::VALUE_REQUIRED, 'The path to the cache file.'),
new InputOption('diff', '', InputOption::VALUE_NONE, 'Also produce diff for each file.'),
new InputOption('format', '', InputOption::VALUE_REQUIRED, 'To output results in other formats.'),
new InputOption('stop-on-violation', '', InputOption::VALUE_NONE, 'Stop execution on first violation.'),
new InputOption('show-progress', '', InputOption::VALUE_REQUIRED, 'Type of progress indicator (none, dots).'),
]
)
->setDescription('Fixes a directory or a file.')
;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$verbosity = $output->getVerbosity();
$passedConfig = $input->getOption('config');
$passedRules = $input->getOption('rules');
if (null !== $passedConfig && null !== $passedRules) {
throw new InvalidConfigurationException('Passing both `--config` and `--rules` options is not allowed.');
}
$resolver = new ConfigurationResolver(
$this->defaultConfig,
[
'allow-risky' => $input->getOption('allow-risky'),
'config' => $passedConfig,
'dry-run' => $input->getOption('dry-run'),
'rules' => $passedRules,
'path' => $input->getArgument('path'),
'path-mode' => $input->getOption('path-mode'),
'using-cache' => $input->getOption('using-cache'),
'cache-file' => $input->getOption('cache-file'),
'format' => $input->getOption('format'),
'diff' => $input->getOption('diff'),
'stop-on-violation' => $input->getOption('stop-on-violation'),
'verbosity' => $verbosity,
'show-progress' => $input->getOption('show-progress'),
],
getcwd(),
$this->toolInfo
);
$reporter = $resolver->getReporter();
$stdErr = $output instanceof ConsoleOutputInterface
? $output->getErrorOutput()
: ('txt' === $reporter->getFormat() ? $output : null)
;
if (null !== $stdErr) {
if (OutputInterface::VERBOSITY_VERBOSE <= $verbosity) {
$stdErr->writeln($this->getApplication()->getLongVersion());
}
$configFile = $resolver->getConfigFile();
$stdErr->writeln(sprintf('Loaded config <comment>%s</comment>%s.', $resolver->getConfig()->getName(), null === $configFile ? '' : ' from "'.$configFile.'"'));
if ($resolver->getUsingCache()) {
$cacheFile = $resolver->getCacheFile();
if (is_file($cacheFile)) {
$stdErr->writeln(sprintf('Using cache file "%s".', $cacheFile));
}
}
}
$progressType = $resolver->getProgress();
$finder = $resolver->getFinder();
if (null !== $stdErr && $resolver->configFinderIsOverridden()) {
$stdErr->writeln(
sprintf($stdErr->isDecorated() ? '<bg=yellow;fg=black;>%s</>' : '%s', 'Paths from configuration file have been overridden by paths provided as command arguments.')
);
}
if ('none' === $progressType || null === $stdErr) {
$progressOutput = new NullOutput();
} else {
$finder = new \ArrayIterator(iterator_to_array($finder));
$progressOutput = new ProcessOutput(
$stdErr,
$this->eventDispatcher,
(new Terminal())->getWidth(),
\count($finder)
);
}
$runner = new Runner(
$finder,
$resolver->getFixers(),
$resolver->getDiffer(),
'none' !== $progressType ? $this->eventDispatcher : null,
$this->errorsManager,
$resolver->getLinter(),
$resolver->isDryRun(),
$resolver->getCacheManager(),
$resolver->getDirectory(),
$resolver->shouldStopOnViolation()
);
$this->stopwatch->start('fixFiles');
$changed = $runner->fix();
$this->stopwatch->stop('fixFiles');
$progressOutput->printLegend();
$fixEvent = $this->stopwatch->getEvent('fixFiles');
$reportSummary = new ReportSummary(
$changed,
$fixEvent->getDuration(),
$fixEvent->getMemory(),
OutputInterface::VERBOSITY_VERBOSE <= $verbosity,
$resolver->isDryRun(),
$output->isDecorated()
);
$output->isDecorated()
? $output->write($reporter->generate($reportSummary))
: $output->write($reporter->generate($reportSummary), false, OutputInterface::OUTPUT_RAW)
;
$invalidErrors = $this->errorsManager->getInvalidErrors();
$exceptionErrors = $this->errorsManager->getExceptionErrors();
$lintErrors = $this->errorsManager->getLintErrors();
if (null !== $stdErr) {
$errorOutput = new ErrorOutput($stdErr);
if (\count($invalidErrors) > 0) {
$errorOutput->listErrors('linting before fixing', $invalidErrors);
}
if (\count($exceptionErrors) > 0) {
$errorOutput->listErrors('fixing', $exceptionErrors);
}
if (\count($lintErrors) > 0) {
$errorOutput->listErrors('linting after fixing', $lintErrors);
}
}
$exitStatusCalculator = new FixCommandExitStatusCalculator();
return $exitStatusCalculator->calculate(
$resolver->isDryRun(),
\count($changed) > 0,
\count($invalidErrors) > 0,
\count($exceptionErrors) > 0,
\count($lintErrors) > 0
);
}
}
@@ -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\Console\Command;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class FixCommandExitStatusCalculator
{
// Exit status 1 is reserved for environment constraints not matched.
public const EXIT_STATUS_FLAG_HAS_INVALID_FILES = 4;
public const EXIT_STATUS_FLAG_HAS_CHANGED_FILES = 8;
public const EXIT_STATUS_FLAG_HAS_INVALID_CONFIG = 16;
public const EXIT_STATUS_FLAG_HAS_INVALID_FIXER_CONFIG = 32;
public const EXIT_STATUS_FLAG_EXCEPTION_IN_APP = 64;
public function calculate(bool $isDryRun, bool $hasChangedFiles, bool $hasInvalidErrors, bool $hasExceptionErrors, bool $hasLintErrorsAfterFixing): int
{
$exitStatus = 0;
if ($isDryRun) {
if ($hasChangedFiles) {
$exitStatus |= self::EXIT_STATUS_FLAG_HAS_CHANGED_FILES;
}
if ($hasInvalidErrors) {
$exitStatus |= self::EXIT_STATUS_FLAG_HAS_INVALID_FILES;
}
}
if ($hasExceptionErrors || $hasLintErrorsAfterFixing) {
$exitStatus |= self::EXIT_STATUS_FLAG_EXCEPTION_IN_APP;
}
return $exitStatus;
}
}
@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Console\Command;
use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
use PhpCsFixer\FixerConfiguration\FixerOptionInterface;
use PhpCsFixer\Preg;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\HelpCommand as BaseHelpCommand;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
#[AsCommand(name: 'help')]
final class HelpCommand extends BaseHelpCommand
{
/**
* @var string
*/
protected static $defaultName = 'help';
/**
* @param mixed $value
*/
public static function toString($value): string
{
return \is_array($value)
? static::arrayToString($value)
: static::scalarToString($value)
;
}
/**
* Returns the allowed values of the given option that can be converted to a string.
*
* @return null|list<AllowedValueSubset|mixed>
*/
public static function getDisplayableAllowedValues(FixerOptionInterface $option): ?array
{
$allowed = $option->getAllowedValues();
if (null !== $allowed) {
$allowed = array_filter($allowed, static function ($value): bool {
return !$value instanceof \Closure;
});
usort($allowed, static function ($valueA, $valueB): int {
if ($valueA instanceof AllowedValueSubset) {
return -1;
}
if ($valueB instanceof AllowedValueSubset) {
return 1;
}
return strcasecmp(
self::toString($valueA),
self::toString($valueB)
);
});
if (0 === \count($allowed)) {
$allowed = null;
}
}
return $allowed;
}
/**
* {@inheritdoc}
*/
protected function initialize(InputInterface $input, OutputInterface $output): void
{
$output->getFormatter()->setStyle('url', new OutputFormatterStyle('blue'));
}
/**
* @param mixed $value
*/
private static function scalarToString($value): string
{
$str = var_export($value, true);
return Preg::replace('/\bNULL\b/', 'null', $str);
}
/**
* @param array<mixed> $value
*/
private static function arrayToString(array $value): string
{
if (0 === \count($value)) {
return '[]';
}
$isHash = !array_is_list($value);
$str = '[';
foreach ($value as $k => $v) {
if ($isHash) {
$str .= static::scalarToString($k).' => ';
}
$str .= \is_array($v)
? static::arrayToString($v).', '
: static::scalarToString($v).', '
;
}
return substr($str, 0, -2).']';
}
}
@@ -0,0 +1,96 @@
<?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\Console\Command;
use PhpCsFixer\Config;
use PhpCsFixer\ConfigInterface;
use PhpCsFixer\Console\ConfigurationResolver;
use PhpCsFixer\ToolInfoInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Markus Staab <markus.staab@redaxo.org>
*
* @internal
*/
#[AsCommand(name: 'list-files')]
final class ListFilesCommand extends Command
{
/**
* @var string
*/
protected static $defaultName = 'list-files';
private ConfigInterface $defaultConfig;
private ToolInfoInterface $toolInfo;
public function __construct(ToolInfoInterface $toolInfo)
{
parent::__construct();
$this->defaultConfig = new Config();
$this->toolInfo = $toolInfo;
}
/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this
->setDefinition(
[
new InputOption('config', '', InputOption::VALUE_REQUIRED, 'The path to a .php-cs-fixer.php file.'),
]
)
->setDescription('List all files being fixed by the given config.')
;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$passedConfig = $input->getOption('config');
$cwd = getcwd();
$resolver = new ConfigurationResolver(
$this->defaultConfig,
[
'config' => $passedConfig,
],
$cwd,
$this->toolInfo
);
$finder = $resolver->getFinder();
/** @var \SplFileInfo $file */
foreach ($finder as $file) {
if ($file->isFile()) {
$relativePath = str_replace($cwd, '.', $file->getRealPath());
// unify directory separators across operating system
$relativePath = str_replace('/', \DIRECTORY_SEPARATOR, $relativePath);
$output->writeln(escapeshellarg($relativePath));
}
}
return 0;
}
}
@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Console\Command;
use PhpCsFixer\ConfigurationException\InvalidConfigurationException;
use PhpCsFixer\Console\Report\ListSetsReport\ReporterFactory;
use PhpCsFixer\Console\Report\ListSetsReport\ReporterInterface;
use PhpCsFixer\Console\Report\ListSetsReport\ReportSummary;
use PhpCsFixer\Console\Report\ListSetsReport\TextReporter;
use PhpCsFixer\RuleSet\RuleSets;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
#[AsCommand(name: 'list-sets')]
final class ListSetsCommand extends Command
{
/**
* @var string
*/
protected static $defaultName = 'list-sets';
/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this
->setDefinition(
[
new InputOption('format', '', InputOption::VALUE_REQUIRED, 'To output results in other formats.', (new TextReporter())->getFormat()),
]
)
->setDescription('List all available RuleSets.')
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$reporter = $this->resolveReporterWithFactory(
$input->getOption('format'),
new ReporterFactory()
);
$reportSummary = new ReportSummary(
array_values(RuleSets::getSetDefinitions())
);
$report = $reporter->generate($reportSummary);
$output->isDecorated()
? $output->write(OutputFormatter::escape($report))
: $output->write($report, false, OutputInterface::OUTPUT_RAW)
;
return 0;
}
private function resolveReporterWithFactory(string $format, ReporterFactory $factory): ReporterInterface
{
try {
$factory->registerBuiltInReporters();
$reporter = $factory->getReporter($format);
} catch (\UnexpectedValueException $e) {
$formats = $factory->getFormats();
sort($formats);
throw new InvalidConfigurationException(sprintf('The format "%s" is not defined, supported are "%s".', $format, implode('", "', $formats)));
}
return $reporter;
}
}
@@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Console\Command;
use PhpCsFixer\Console\SelfUpdate\NewVersionCheckerInterface;
use PhpCsFixer\PharCheckerInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\ToolInfoInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @author Igor Wiedler <igor@wiedler.ch>
* @author Stephane PY <py.stephane1@gmail.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
#[AsCommand(name: 'self-update')]
final class SelfUpdateCommand extends Command
{
/**
* @var string
*/
protected static $defaultName = 'self-update';
private NewVersionCheckerInterface $versionChecker;
private ToolInfoInterface $toolInfo;
private PharCheckerInterface $pharChecker;
public function __construct(
NewVersionCheckerInterface $versionChecker,
ToolInfoInterface $toolInfo,
PharCheckerInterface $pharChecker
) {
parent::__construct();
$this->versionChecker = $versionChecker;
$this->toolInfo = $toolInfo;
$this->pharChecker = $pharChecker;
}
/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this
->setAliases(['selfupdate'])
->setDefinition(
[
new InputOption('--force', '-f', InputOption::VALUE_NONE, 'Force update to next major version if available.'),
]
)
->setDescription('Update php-cs-fixer.phar to the latest stable version.')
->setHelp(
<<<'EOT'
The <info>%command.name%</info> command replace your php-cs-fixer.phar by the
latest version released on:
<comment>https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/releases</comment>
<info>$ php php-cs-fixer.phar %command.name%</info>
EOT
)
;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity() && $output instanceof ConsoleOutputInterface) {
$stdErr = $output->getErrorOutput();
$stdErr->writeln($this->getApplication()->getLongVersion());
}
if (!$this->toolInfo->isInstalledAsPhar()) {
$output->writeln('<error>Self-update is available only for PHAR version.</error>');
return 1;
}
$currentVersion = $this->getApplication()->getVersion();
Preg::match('/^v?(?<major>\d+)\./', $currentVersion, $matches);
$currentMajor = (int) $matches['major'];
try {
$latestVersion = $this->versionChecker->getLatestVersion();
$latestVersionOfCurrentMajor = $this->versionChecker->getLatestVersionOfMajor($currentMajor);
} catch (\Exception $exception) {
$output->writeln(sprintf(
'<error>Unable to determine newest version: %s</error>',
$exception->getMessage()
));
return 1;
}
if (1 !== $this->versionChecker->compareVersions($latestVersion, $currentVersion)) {
$output->writeln('<info>PHP CS Fixer is already up-to-date.</info>');
return 0;
}
$remoteTag = $latestVersion;
if (
0 !== $this->versionChecker->compareVersions($latestVersionOfCurrentMajor, $latestVersion)
&& true !== $input->getOption('force')
) {
$output->writeln(sprintf('<info>A new major version of PHP CS Fixer is available</info> (<comment>%s</comment>)', $latestVersion));
$output->writeln(sprintf('<info>Before upgrading please read</info> https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/%s/UPGRADE-v%s.md', $latestVersion, $currentMajor + 1));
$output->writeln('<info>If you are ready to upgrade run this command with</info> <comment>-f</comment>');
$output->writeln('<info>Checking for new minor/patch version...</info>');
if (1 !== $this->versionChecker->compareVersions($latestVersionOfCurrentMajor, $currentVersion)) {
$output->writeln('<info>No minor update for PHP CS Fixer.</info>');
return 0;
}
$remoteTag = $latestVersionOfCurrentMajor;
}
$localFilename = realpath($_SERVER['argv'][0]) ?: $_SERVER['argv'][0];
if (!is_writable($localFilename)) {
$output->writeln(sprintf('<error>No permission to update</error> "%s" <error>file.</error>', $localFilename));
return 1;
}
$tempFilename = \dirname($localFilename).'/'.basename($localFilename, '.phar').'-tmp.phar';
$remoteFilename = $this->toolInfo->getPharDownloadUri($remoteTag);
if (false === @copy($remoteFilename, $tempFilename)) {
$output->writeln(sprintf('<error>Unable to download new version</error> %s <error>from the server.</error>', $remoteTag));
return 1;
}
chmod($tempFilename, 0777 & ~umask());
$pharInvalidityReason = $this->pharChecker->checkFileValidity($tempFilename);
if (null !== $pharInvalidityReason) {
unlink($tempFilename);
$output->writeln(sprintf('<error>The download of</error> %s <error>is corrupt (%s).</error>', $remoteTag, $pharInvalidityReason));
$output->writeln('<error>Please re-run the "self-update" command to try again.</error>');
return 1;
}
rename($tempFilename, $localFilename);
$output->writeln(sprintf('<info>PHP CS Fixer updated</info> (<comment>%s</comment> -> <comment>%s</comment>)', $currentVersion, $remoteTag));
return 0;
}
}
@@ -0,0 +1,961 @@
<?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\Console;
use PhpCsFixer\Cache\CacheManagerInterface;
use PhpCsFixer\Cache\Directory;
use PhpCsFixer\Cache\DirectoryInterface;
use PhpCsFixer\Cache\FileCacheManager;
use PhpCsFixer\Cache\FileHandler;
use PhpCsFixer\Cache\NullCacheManager;
use PhpCsFixer\Cache\Signature;
use PhpCsFixer\ConfigInterface;
use PhpCsFixer\ConfigurationException\InvalidConfigurationException;
use PhpCsFixer\Console\Command\HelpCommand;
use PhpCsFixer\Console\Report\FixReport\ReporterFactory;
use PhpCsFixer\Console\Report\FixReport\ReporterInterface;
use PhpCsFixer\Differ\DifferInterface;
use PhpCsFixer\Differ\NullDiffer;
use PhpCsFixer\Differ\UnifiedDiffer;
use PhpCsFixer\Finder;
use PhpCsFixer\Fixer\DeprecatedFixerInterface;
use PhpCsFixer\Fixer\FixerInterface;
use PhpCsFixer\FixerFactory;
use PhpCsFixer\Linter\Linter;
use PhpCsFixer\Linter\LinterInterface;
use PhpCsFixer\RuleSet\RuleSet;
use PhpCsFixer\RuleSet\RuleSetInterface;
use PhpCsFixer\StdinFileInfo;
use PhpCsFixer\ToolInfoInterface;
use PhpCsFixer\Utils;
use PhpCsFixer\WhitespacesFixerConfig;
use PhpCsFixer\WordMatcher;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder as SymfonyFinder;
/**
* The resolver that resolves configuration to use by command line options and config.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Katsuhiro Ogawa <ko.fivestar@gmail.com>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class ConfigurationResolver
{
public const PATH_MODE_OVERRIDE = 'override';
public const PATH_MODE_INTERSECTION = 'intersection';
/**
* @var null|bool
*/
private $allowRisky;
/**
* @var null|ConfigInterface
*/
private $config;
/**
* @var null|string
*/
private $configFile;
private string $cwd;
private ConfigInterface $defaultConfig;
/**
* @var null|ReporterInterface
*/
private $reporter;
/**
* @var null|bool
*/
private $isStdIn;
/**
* @var null|bool
*/
private $isDryRun;
/**
* @var null|FixerInterface[]
*/
private $fixers;
/**
* @var null|bool
*/
private $configFinderIsOverridden;
private ToolInfoInterface $toolInfo;
/**
* @var array<string, mixed>
*/
private array $options = [
'allow-risky' => null,
'cache-file' => null,
'config' => null,
'diff' => null,
'dry-run' => null,
'format' => null,
'path' => [],
'path-mode' => self::PATH_MODE_OVERRIDE,
'rules' => null,
'show-progress' => null,
'stop-on-violation' => null,
'using-cache' => null,
'verbosity' => null,
];
/**
* @var null|string
*/
private $cacheFile;
/**
* @var null|CacheManagerInterface
*/
private $cacheManager;
/**
* @var null|DifferInterface
*/
private $differ;
/**
* @var null|Directory
*/
private $directory;
/**
* @var null|iterable<\SplFileInfo>
*/
private ?iterable $finder = null;
private ?string $format = null;
/**
* @var null|Linter
*/
private $linter;
/**
* @var null|list<string>
*/
private ?array $path = null;
/**
* @var null|string
*/
private $progress;
/**
* @var null|RuleSet
*/
private $ruleSet;
/**
* @var null|bool
*/
private $usingCache;
/**
* @var FixerFactory
*/
private $fixerFactory;
/**
* @param array<string, mixed> $options
*/
public function __construct(
ConfigInterface $config,
array $options,
string $cwd,
ToolInfoInterface $toolInfo
) {
$this->defaultConfig = $config;
$this->cwd = $cwd;
$this->toolInfo = $toolInfo;
foreach ($options as $name => $value) {
$this->setOption($name, $value);
}
}
public function getCacheFile(): ?string
{
if (!$this->getUsingCache()) {
return null;
}
if (null === $this->cacheFile) {
if (null === $this->options['cache-file']) {
$this->cacheFile = $this->getConfig()->getCacheFile();
} else {
$this->cacheFile = $this->options['cache-file'];
}
}
return $this->cacheFile;
}
public function getCacheManager(): CacheManagerInterface
{
if (null === $this->cacheManager) {
$cacheFile = $this->getCacheFile();
if (null === $cacheFile) {
$this->cacheManager = new NullCacheManager();
} else {
$this->cacheManager = new FileCacheManager(
new FileHandler($cacheFile),
new Signature(
PHP_VERSION,
$this->toolInfo->getVersion(),
$this->getConfig()->getIndent(),
$this->getConfig()->getLineEnding(),
$this->getRules()
),
$this->isDryRun(),
$this->getDirectory()
);
}
}
return $this->cacheManager;
}
public function getConfig(): ConfigInterface
{
if (null === $this->config) {
foreach ($this->computeConfigFiles() as $configFile) {
if (!file_exists($configFile)) {
continue;
}
$configFileBasename = basename($configFile);
$deprecatedConfigs = [
'.php_cs' => '.php-cs-fixer.php',
'.php_cs.dist' => '.php-cs-fixer.dist.php',
];
if (isset($deprecatedConfigs[$configFileBasename])) {
throw new InvalidConfigurationException("Configuration file `{$configFileBasename}` is outdated, rename to `{$deprecatedConfigs[$configFileBasename]}`.");
}
$this->config = self::separatedContextLessInclude($configFile);
$this->configFile = $configFile;
break;
}
if (null === $this->config) {
$this->config = $this->defaultConfig;
}
}
return $this->config;
}
public function getConfigFile(): ?string
{
if (null === $this->configFile) {
$this->getConfig();
}
return $this->configFile;
}
public function getDiffer(): DifferInterface
{
if (null === $this->differ) {
if ($this->options['diff']) {
$this->differ = new UnifiedDiffer();
} else {
$this->differ = new NullDiffer();
}
}
return $this->differ;
}
public function getDirectory(): DirectoryInterface
{
if (null === $this->directory) {
$path = $this->getCacheFile();
if (null === $path) {
$absolutePath = $this->cwd;
} else {
$filesystem = new Filesystem();
$absolutePath = $filesystem->isAbsolutePath($path)
? $path
: $this->cwd.\DIRECTORY_SEPARATOR.$path;
}
$this->directory = new Directory(\dirname($absolutePath));
}
return $this->directory;
}
/**
* @return FixerInterface[] An array of FixerInterface
*/
public function getFixers(): array
{
if (null === $this->fixers) {
$this->fixers = $this->createFixerFactory()
->useRuleSet($this->getRuleSet())
->setWhitespacesConfig(new WhitespacesFixerConfig($this->config->getIndent(), $this->config->getLineEnding()))
->getFixers()
;
if (false === $this->getRiskyAllowed()) {
$riskyFixers = array_map(
static function (FixerInterface $fixer): string {
return $fixer->getName();
},
array_filter(
$this->fixers,
static function (FixerInterface $fixer): bool {
return $fixer->isRisky();
}
)
);
if (\count($riskyFixers) > 0) {
throw new InvalidConfigurationException(sprintf('The rules contain risky fixers ("%s"), but they are not allowed to run. Perhaps you forget to use --allow-risky=yes option?', implode('", "', $riskyFixers)));
}
}
}
return $this->fixers;
}
public function getLinter(): LinterInterface
{
if (null === $this->linter) {
$this->linter = new Linter();
}
return $this->linter;
}
/**
* Returns path.
*
* @return string[]
*/
public function getPath(): array
{
if (null === $this->path) {
$filesystem = new Filesystem();
$cwd = $this->cwd;
if (1 === \count($this->options['path']) && '-' === $this->options['path'][0]) {
$this->path = $this->options['path'];
} else {
$this->path = array_map(
static function (string $rawPath) use ($cwd, $filesystem): string {
$path = trim($rawPath);
if ('' === $path) {
throw new InvalidConfigurationException("Invalid path: \"{$rawPath}\".");
}
$absolutePath = $filesystem->isAbsolutePath($path)
? $path
: $cwd.\DIRECTORY_SEPARATOR.$path;
if (!file_exists($absolutePath)) {
throw new InvalidConfigurationException(sprintf(
'The path "%s" is not readable.',
$path
));
}
return $absolutePath;
},
$this->options['path']
);
}
}
return $this->path;
}
/**
* @throws InvalidConfigurationException
*/
public function getProgress(): string
{
if (null === $this->progress) {
if (OutputInterface::VERBOSITY_VERBOSE <= $this->options['verbosity'] && 'txt' === $this->getFormat()) {
$progressType = $this->options['show-progress'];
$progressTypes = ['none', 'dots'];
if (null === $progressType) {
$progressType = $this->getConfig()->getHideProgress() ? 'none' : 'dots';
} elseif (!\in_array($progressType, $progressTypes, true)) {
throw new InvalidConfigurationException(sprintf(
'The progress type "%s" is not defined, supported are "%s".',
$progressType,
implode('", "', $progressTypes)
));
}
$this->progress = $progressType;
} else {
$this->progress = 'none';
}
}
return $this->progress;
}
public function getReporter(): ReporterInterface
{
if (null === $this->reporter) {
$reporterFactory = new ReporterFactory();
$reporterFactory->registerBuiltInReporters();
$format = $this->getFormat();
try {
$this->reporter = $reporterFactory->getReporter($format);
} catch (\UnexpectedValueException $e) {
$formats = $reporterFactory->getFormats();
sort($formats);
throw new InvalidConfigurationException(sprintf('The format "%s" is not defined, supported are "%s".', $format, implode('", "', $formats)));
}
}
return $this->reporter;
}
public function getRiskyAllowed(): bool
{
if (null === $this->allowRisky) {
if (null === $this->options['allow-risky']) {
$this->allowRisky = $this->getConfig()->getRiskyAllowed();
} else {
$this->allowRisky = $this->resolveOptionBooleanValue('allow-risky');
}
}
return $this->allowRisky;
}
/**
* Returns rules.
*
* @return array<string, array<string, mixed>|bool>
*/
public function getRules(): array
{
return $this->getRuleSet()->getRules();
}
public function getUsingCache(): bool
{
if (null === $this->usingCache) {
if (null === $this->options['using-cache']) {
$this->usingCache = $this->getConfig()->getUsingCache();
} else {
$this->usingCache = $this->resolveOptionBooleanValue('using-cache');
}
}
$this->usingCache = $this->usingCache && ($this->toolInfo->isInstalledAsPhar() || $this->toolInfo->isInstalledByComposer());
return $this->usingCache;
}
/**
* @return iterable<\SplFileInfo>
*/
public function getFinder(): iterable
{
if (null === $this->finder) {
$this->finder = $this->resolveFinder();
}
return $this->finder;
}
/**
* Returns dry-run flag.
*/
public function isDryRun(): bool
{
if (null === $this->isDryRun) {
if ($this->isStdIn()) {
// Can't write to STDIN
$this->isDryRun = true;
} else {
$this->isDryRun = $this->options['dry-run'];
}
}
return $this->isDryRun;
}
public function shouldStopOnViolation(): bool
{
return $this->options['stop-on-violation'];
}
public function configFinderIsOverridden(): bool
{
if (null === $this->configFinderIsOverridden) {
$this->resolveFinder();
}
return $this->configFinderIsOverridden;
}
/**
* Compute file candidates for config file.
*
* @return string[]
*/
private function computeConfigFiles(): array
{
$configFile = $this->options['config'];
if (null !== $configFile) {
if (false === file_exists($configFile) || false === is_readable($configFile)) {
throw new InvalidConfigurationException(sprintf('Cannot read config file "%s".', $configFile));
}
return [$configFile];
}
$path = $this->getPath();
if ($this->isStdIn() || 0 === \count($path)) {
$configDir = $this->cwd;
} elseif (1 < \count($path)) {
throw new InvalidConfigurationException('For multiple paths config parameter is required.');
} elseif (!is_file($path[0])) {
$configDir = $path[0];
} else {
$dirName = pathinfo($path[0], PATHINFO_DIRNAME);
$configDir = $dirName ?: $path[0];
}
$candidates = [
$configDir.\DIRECTORY_SEPARATOR.'.php-cs-fixer.php',
$configDir.\DIRECTORY_SEPARATOR.'.php-cs-fixer.dist.php',
$configDir.\DIRECTORY_SEPARATOR.'.php_cs', // old v2 config, present here only to throw nice error message later
$configDir.\DIRECTORY_SEPARATOR.'.php_cs.dist', // old v2 config, present here only to throw nice error message later
];
if ($configDir !== $this->cwd) {
$candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php-cs-fixer.php';
$candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php-cs-fixer.dist.php';
$candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php_cs'; // old v2 config, present here only to throw nice error message later
$candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php_cs.dist'; // old v2 config, present here only to throw nice error message later
}
return $candidates;
}
private function createFixerFactory(): FixerFactory
{
if (null === $this->fixerFactory) {
$fixerFactory = new FixerFactory();
$fixerFactory->registerBuiltInFixers();
$fixerFactory->registerCustomFixers($this->getConfig()->getCustomFixers());
$this->fixerFactory = $fixerFactory;
}
return $this->fixerFactory;
}
private function getFormat(): string
{
if (null === $this->format) {
$this->format = $this->options['format'] ?? $this->getConfig()->getFormat();
}
return $this->format;
}
private function getRuleSet(): RuleSetInterface
{
if (null === $this->ruleSet) {
$rules = $this->parseRules();
$this->validateRules($rules);
$this->ruleSet = new RuleSet($rules);
}
return $this->ruleSet;
}
private function isStdIn(): bool
{
if (null === $this->isStdIn) {
$this->isStdIn = 1 === \count($this->options['path']) && '-' === $this->options['path'][0];
}
return $this->isStdIn;
}
/**
* @template T
*
* @param iterable<T> $iterable
*
* @return \Traversable<T>
*/
private function iterableToTraversable(iterable $iterable): \Traversable
{
return \is_array($iterable) ? new \ArrayIterator($iterable) : $iterable;
}
/**
* @return array<mixed>
*/
private function parseRules(): array
{
if (null === $this->options['rules']) {
return $this->getConfig()->getRules();
}
$rules = trim($this->options['rules']);
if ('' === $rules) {
throw new InvalidConfigurationException('Empty rules value is not allowed.');
}
if (str_starts_with($rules, '{')) {
$rules = json_decode($rules, true);
if (JSON_ERROR_NONE !== json_last_error()) {
throw new InvalidConfigurationException(sprintf('Invalid JSON rules input: "%s".', json_last_error_msg()));
}
return $rules;
}
$rules = [];
foreach (explode(',', $this->options['rules']) as $rule) {
$rule = trim($rule);
if ('' === $rule) {
throw new InvalidConfigurationException('Empty rule name is not allowed.');
}
if (str_starts_with($rule, '-')) {
$rules[substr($rule, 1)] = false;
} else {
$rules[$rule] = true;
}
}
return $rules;
}
/**
* @param array<mixed> $rules
*
* @throws InvalidConfigurationException
*/
private function validateRules(array $rules): void
{
/**
* Create a ruleset that contains all configured rules, even when they originally have been disabled.
*
* @see RuleSet::resolveSet()
*/
$ruleSet = [];
foreach ($rules as $key => $value) {
if (\is_int($key)) {
throw new InvalidConfigurationException(sprintf('Missing value for "%s" rule/set.', $value));
}
$ruleSet[$key] = true;
}
$ruleSet = new RuleSet($ruleSet);
$configuredFixers = array_keys($ruleSet->getRules());
$fixers = $this->createFixerFactory()->getFixers();
$availableFixers = array_map(static fn (FixerInterface $fixer): string => $fixer->getName(), $fixers);
$unknownFixers = array_diff($configuredFixers, $availableFixers);
if (\count($unknownFixers) > 0) {
$renamedRules = [
'blank_line_before_return' => [
'new_name' => 'blank_line_before_statement',
'config' => ['statements' => ['return']],
],
'final_static_access' => [
'new_name' => 'self_static_accessor',
],
'hash_to_slash_comment' => [
'new_name' => 'single_line_comment_style',
'config' => ['comment_types' => ['hash']],
],
'lowercase_constants' => [
'new_name' => 'constant_case',
'config' => ['case' => 'lower'],
],
'no_extra_consecutive_blank_lines' => [
'new_name' => 'no_extra_blank_lines',
],
'no_multiline_whitespace_before_semicolons' => [
'new_name' => 'multiline_whitespace_before_semicolons',
],
'no_short_echo_tag' => [
'new_name' => 'echo_tag_syntax',
'config' => ['format' => 'long'],
],
'php_unit_ordered_covers' => [
'new_name' => 'phpdoc_order_by_value',
'config' => ['annotations' => ['covers']],
],
'phpdoc_inline_tag' => [
'new_name' => 'general_phpdoc_tag_rename, phpdoc_inline_tag_normalizer and phpdoc_tag_type',
],
'pre_increment' => [
'new_name' => 'increment_style',
'config' => ['style' => 'pre'],
],
'psr0' => [
'new_name' => 'psr_autoloading',
'config' => ['dir' => 'x'],
],
'psr4' => [
'new_name' => 'psr_autoloading',
],
'silenced_deprecation_error' => [
'new_name' => 'error_suppression',
],
'trailing_comma_in_multiline_array' => [
'new_name' => 'trailing_comma_in_multiline',
'config' => ['elements' => ['arrays']],
],
];
$message = 'The rules contain unknown fixers: ';
$hasOldRule = false;
foreach ($unknownFixers as $unknownFixer) {
if (isset($renamedRules[$unknownFixer])) { // Check if present as old renamed rule
$hasOldRule = true;
$message .= sprintf(
'"%s" is renamed (did you mean "%s"?%s), ',
$unknownFixer,
$renamedRules[$unknownFixer]['new_name'],
isset($renamedRules[$unknownFixer]['config']) ? ' (note: use configuration "'.HelpCommand::toString($renamedRules[$unknownFixer]['config']).'")' : ''
);
} else { // Go to normal matcher if it is not a renamed rule
$matcher = new WordMatcher($availableFixers);
$alternative = $matcher->match($unknownFixer);
$message .= sprintf(
'"%s"%s, ',
$unknownFixer,
null === $alternative ? '' : ' (did you mean "'.$alternative.'"?)'
);
}
}
$message = substr($message, 0, -2).'.';
if ($hasOldRule) {
$message .= "\nFor more info about updating see: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/v3.0.0/UPGRADE-v3.md#renamed-ruless.";
}
throw new InvalidConfigurationException($message);
}
foreach ($fixers as $fixer) {
$fixerName = $fixer->getName();
if (isset($rules[$fixerName]) && $fixer instanceof DeprecatedFixerInterface) {
$successors = $fixer->getSuccessorsNames();
$messageEnd = [] === $successors
? sprintf(' and will be removed in version %d.0.', Application::getMajorVersion() + 1)
: sprintf('. Use %s instead.', str_replace('`', '"', Utils::naturalLanguageJoinWithBackticks($successors)));
Utils::triggerDeprecation(new \RuntimeException("Rule \"{$fixerName}\" is deprecated{$messageEnd}"));
}
}
}
/**
* Apply path on config instance.
*
* @return iterable<\SplFileInfo>
*/
private function resolveFinder(): iterable
{
$this->configFinderIsOverridden = false;
if ($this->isStdIn()) {
return new \ArrayIterator([new StdinFileInfo()]);
}
$modes = [self::PATH_MODE_OVERRIDE, self::PATH_MODE_INTERSECTION];
if (!\in_array(
$this->options['path-mode'],
$modes,
true
)) {
throw new InvalidConfigurationException(sprintf(
'The path-mode "%s" is not defined, supported are "%s".',
$this->options['path-mode'],
implode('", "', $modes)
));
}
$isIntersectionPathMode = self::PATH_MODE_INTERSECTION === $this->options['path-mode'];
$paths = array_filter(array_map(
static function (string $path) {
return realpath($path);
},
$this->getPath()
));
if (0 === \count($paths)) {
if ($isIntersectionPathMode) {
return new \ArrayIterator([]);
}
return $this->iterableToTraversable($this->getConfig()->getFinder());
}
$pathsByType = [
'file' => [],
'dir' => [],
];
foreach ($paths as $path) {
if (is_file($path)) {
$pathsByType['file'][] = $path;
} else {
$pathsByType['dir'][] = $path.\DIRECTORY_SEPARATOR;
}
}
$nestedFinder = null;
$currentFinder = $this->iterableToTraversable($this->getConfig()->getFinder());
try {
$nestedFinder = $currentFinder instanceof \IteratorAggregate ? $currentFinder->getIterator() : $currentFinder;
} catch (\Exception $e) {
}
if ($isIntersectionPathMode) {
if (null === $nestedFinder) {
throw new InvalidConfigurationException(
'Cannot create intersection with not-fully defined Finder in configuration file.'
);
}
return new \CallbackFilterIterator(
new \IteratorIterator($nestedFinder),
static function (\SplFileInfo $current) use ($pathsByType): bool {
$currentRealPath = $current->getRealPath();
if (\in_array($currentRealPath, $pathsByType['file'], true)) {
return true;
}
foreach ($pathsByType['dir'] as $path) {
if (str_starts_with($currentRealPath, $path)) {
return true;
}
}
return false;
}
);
}
if (null !== $this->getConfigFile() && null !== $nestedFinder) {
$this->configFinderIsOverridden = true;
}
if ($currentFinder instanceof SymfonyFinder && null === $nestedFinder) {
// finder from configuration Symfony finder and it is not fully defined, we may fulfill it
return $currentFinder->in($pathsByType['dir'])->append($pathsByType['file']);
}
return Finder::create()->in($pathsByType['dir'])->append($pathsByType['file']);
}
/**
* Set option that will be resolved.
*
* @param mixed $value
*/
private function setOption(string $name, $value): void
{
if (!\array_key_exists($name, $this->options)) {
throw new InvalidConfigurationException(sprintf('Unknown option name: "%s".', $name));
}
$this->options[$name] = $value;
}
private function resolveOptionBooleanValue(string $optionName): bool
{
$value = $this->options[$optionName];
if (!\is_string($value)) {
throw new InvalidConfigurationException(sprintf('Expected boolean or string value for option "%s".', $optionName));
}
if ('yes' === $value) {
return true;
}
if ('no' === $value) {
return false;
}
throw new InvalidConfigurationException(sprintf('Expected "yes" or "no" for option "%s", got "%s".', $optionName, $value));
}
private static function separatedContextLessInclude(string $path): ConfigInterface
{
$config = include $path;
// verify that the config has an instance of Config
if (!$config instanceof ConfigInterface) {
throw new InvalidConfigurationException(sprintf('The config file: "%s" does not return a "PhpCsFixer\ConfigInterface" instance. Got: "%s".', $path, \is_object($config) ? \get_class($config) : \gettype($config)));
}
return $config;
}
}
@@ -0,0 +1,156 @@
<?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\Console\Output;
use PhpCsFixer\Differ\DiffConsoleFormatter;
use PhpCsFixer\Error\Error;
use PhpCsFixer\Linter\LintingException;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
final class ErrorOutput
{
private OutputInterface $output;
/**
* @var bool
*/
private $isDecorated;
public function __construct(OutputInterface $output)
{
$this->output = $output;
$this->isDecorated = $output->isDecorated();
}
/**
* @param Error[] $errors
*/
public function listErrors(string $process, array $errors): void
{
$this->output->writeln(['', sprintf(
'Files that were not fixed due to errors reported during %s:',
$process
)]);
$showDetails = $this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE;
$showTrace = $this->output->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG;
foreach ($errors as $i => $error) {
$this->output->writeln(sprintf('%4d) %s', $i + 1, $error->getFilePath()));
$e = $error->getSource();
if (!$showDetails || null === $e) {
continue;
}
$class = sprintf('[%s]', \get_class($e));
$message = $e->getMessage();
$code = $e->getCode();
if (0 !== $code) {
$message .= " ({$code})";
}
$length = max(\strlen($class), \strlen($message));
$lines = [
'',
$class,
$message,
'',
];
$this->output->writeln('');
foreach ($lines as $line) {
if (\strlen($line) < $length) {
$line .= str_repeat(' ', $length - \strlen($line));
}
$this->output->writeln(sprintf(' <error> %s </error>', $this->prepareOutput($line)));
}
if ($showTrace && !$e instanceof LintingException) { // stack trace of lint exception is of no interest
$this->output->writeln('');
$stackTrace = $e->getTrace();
foreach ($stackTrace as $trace) {
if (isset($trace['class']) && \Symfony\Component\Console\Command\Command::class === $trace['class'] && 'run' === $trace['function']) {
$this->output->writeln(' [ ... ]');
break;
}
$this->outputTrace($trace);
}
}
if (Error::TYPE_LINT === $error->getType() && 0 < \count($error->getAppliedFixers())) {
$this->output->writeln('');
$this->output->writeln(sprintf(' Applied fixers: <comment>%s</comment>', implode(', ', $error->getAppliedFixers())));
$diff = $error->getDiff();
if (!empty($diff)) {
$diffFormatter = new DiffConsoleFormatter(
$this->isDecorated,
sprintf(
'<comment> ---------- begin diff ----------</comment>%s%%s%s<comment> ----------- end diff -----------</comment>',
PHP_EOL,
PHP_EOL
)
);
$this->output->writeln($diffFormatter->format($diff));
}
}
}
}
/**
* @param array{
* function?: string,
* line?: int,
* file?: string,
* class?: class-string,
* type?: '::'|'->',
* args?: mixed[],
* object?: object,
* } $trace
*/
private function outputTrace(array $trace): void
{
if (isset($trace['class'], $trace['type'], $trace['function'])) {
$this->output->writeln(sprintf(
' <comment>%s</comment>%s<comment>%s()</comment>',
$this->prepareOutput($trace['class']),
$this->prepareOutput($trace['type']),
$this->prepareOutput($trace['function'])
));
} elseif (isset($trace['function'])) {
$this->output->writeln(sprintf(' <comment>%s()</comment>', $this->prepareOutput($trace['function'])));
}
if (isset($trace['file'])) {
$this->output->writeln(sprintf(' in <info>%s</info> at line <info>%d</info>', $this->prepareOutput($trace['file']), $trace['line']));
}
}
private function prepareOutput(string $string): string
{
return $this->isDecorated
? OutputFormatter::escape($string)
: $string
;
}
}
@@ -0,0 +1,25 @@
<?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\Console\Output;
/**
* @internal
*/
final class NullOutput implements ProcessOutputInterface
{
public function printLegend(): void
{
}
}
@@ -0,0 +1,133 @@
<?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\Console\Output;
use PhpCsFixer\FixerFileProcessedEvent;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Output writer to show the process of a FixCommand.
*
* @internal
*/
final class ProcessOutput implements ProcessOutputInterface
{
/**
* File statuses map.
*
* @var array<FixerFileProcessedEvent::STATUS_*, array{symbol: string, format: string, description: string}>
*/
private static array $eventStatusMap = [
FixerFileProcessedEvent::STATUS_NO_CHANGES => ['symbol' => '.', 'format' => '%s', 'description' => 'no changes'],
FixerFileProcessedEvent::STATUS_FIXED => ['symbol' => 'F', 'format' => '<fg=green>%s</fg=green>', 'description' => 'fixed'],
FixerFileProcessedEvent::STATUS_SKIPPED => ['symbol' => 'S', 'format' => '<fg=cyan>%s</fg=cyan>', 'description' => 'skipped (cached or empty file)'],
FixerFileProcessedEvent::STATUS_INVALID => ['symbol' => 'I', 'format' => '<bg=red>%s</bg=red>', 'description' => 'invalid file syntax (file ignored)'],
FixerFileProcessedEvent::STATUS_EXCEPTION => ['symbol' => 'E', 'format' => '<bg=red>%s</bg=red>', 'description' => 'error'],
FixerFileProcessedEvent::STATUS_LINT => ['symbol' => 'E', 'format' => '<bg=red>%s</bg=red>', 'description' => 'error'],
];
private OutputInterface $output;
private EventDispatcherInterface $eventDispatcher;
private int $files;
private int $processedFiles = 0;
/**
* @var int
*/
private $symbolsPerLine;
public function __construct(OutputInterface $output, EventDispatcherInterface $dispatcher, int $width, int $nbFiles)
{
$this->output = $output;
$this->eventDispatcher = $dispatcher;
$this->eventDispatcher->addListener(FixerFileProcessedEvent::NAME, [$this, 'onFixerFileProcessed']);
$this->files = $nbFiles;
// max number of characters per line
// - total length x 2 (e.g. " 1 / 123" => 6 digits and padding spaces)
// - 11 (extra spaces, parentheses and percentage characters, e.g. " x / x (100%)")
$this->symbolsPerLine = max(1, $width - \strlen((string) $this->files) * 2 - 11);
}
public function __destruct()
{
$this->eventDispatcher->removeListener(FixerFileProcessedEvent::NAME, [$this, 'onFixerFileProcessed']);
}
/**
* This class is not intended to be serialized,
* and cannot be deserialized (see __wakeup method).
*/
public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
/**
* Disable the deserialization of the class to prevent attacker executing
* code by leveraging the __destruct method.
*
* @see https://owasp.org/www-community/vulnerabilities/PHP_Object_Injection
*/
public function __wakeup(): void
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function onFixerFileProcessed(FixerFileProcessedEvent $event): void
{
$status = self::$eventStatusMap[$event->getStatus()];
$this->output->write($this->output->isDecorated() ? sprintf($status['format'], $status['symbol']) : $status['symbol']);
++$this->processedFiles;
$symbolsOnCurrentLine = $this->processedFiles % $this->symbolsPerLine;
$isLast = $this->processedFiles === $this->files;
if (0 === $symbolsOnCurrentLine || $isLast) {
$this->output->write(sprintf(
'%s %'.\strlen((string) $this->files).'d / %d (%3d%%)',
$isLast && 0 !== $symbolsOnCurrentLine ? str_repeat(' ', $this->symbolsPerLine - $symbolsOnCurrentLine) : '',
$this->processedFiles,
$this->files,
round($this->processedFiles / $this->files * 100)
));
if (!$isLast) {
$this->output->writeln('');
}
}
}
public function printLegend(): void
{
$symbols = [];
foreach (self::$eventStatusMap as $status) {
$symbol = $status['symbol'];
if ('' === $symbol || isset($symbols[$symbol])) {
continue;
}
$symbols[$symbol] = sprintf('%s-%s', $this->output->isDecorated() ? sprintf($status['format'], $symbol) : $symbol, $status['description']);
}
$this->output->write(sprintf("\nLegend: %s\n", implode(', ', $symbols)));
}
}
@@ -0,0 +1,23 @@
<?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\Console\Output;
/**
* @internal
*/
interface ProcessOutputInterface
{
public function printLegend(): void;
}
@@ -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\Console\Report\FixReport;
use Symfony\Component\Console\Formatter\OutputFormatter;
/**
* @author Kévin Gomez <contact@kevingomez.fr>
*
* @internal
*/
final class CheckstyleReporter implements ReporterInterface
{
/**
* {@inheritdoc}
*/
public function getFormat(): string
{
return 'checkstyle';
}
/**
* {@inheritdoc}
*/
public function generate(ReportSummary $reportSummary): string
{
if (!\extension_loaded('dom')) {
throw new \RuntimeException('Cannot generate report! `ext-dom` is not available!');
}
$dom = new \DOMDocument('1.0', 'UTF-8');
$checkstyles = $dom->appendChild($dom->createElement('checkstyle'));
foreach ($reportSummary->getChanged() as $filePath => $fixResult) {
/** @var \DOMElement $file */
$file = $checkstyles->appendChild($dom->createElement('file'));
$file->setAttribute('name', $filePath);
foreach ($fixResult['appliedFixers'] as $appliedFixer) {
$error = $this->createError($dom, $appliedFixer);
$file->appendChild($error);
}
}
$dom->formatOutput = true;
return $reportSummary->isDecoratedOutput() ? OutputFormatter::escape($dom->saveXML()) : $dom->saveXML();
}
private function createError(\DOMDocument $dom, string $appliedFixer): \DOMElement
{
$error = $dom->createElement('error');
$error->setAttribute('severity', 'warning');
$error->setAttribute('source', 'PHP-CS-Fixer.'.$appliedFixer);
$error->setAttribute('message', 'Found violation(s) of type: '.$appliedFixer);
return $error;
}
}
@@ -0,0 +1,61 @@
<?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\Console\Report\FixReport;
use Symfony\Component\Console\Formatter\OutputFormatter;
/**
* Generates a report according to gitlabs subset of codeclimate json files.
*
* @see https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#data-types
*
* @author Hans-Christian Otto <c.otto@suora.com>
*
* @internal
*/
final class GitlabReporter implements ReporterInterface
{
public function getFormat(): string
{
return 'gitlab';
}
/**
* Process changed files array. Returns generated report.
*/
public function generate(ReportSummary $reportSummary): string
{
$report = [];
foreach ($reportSummary->getChanged() as $fileName => $change) {
foreach ($change['appliedFixers'] as $fixerName) {
$report[] = [
'description' => $fixerName,
'fingerprint' => md5($fileName.$fixerName),
'severity' => 'minor',
'location' => [
'path' => $fileName,
'lines' => [
'begin' => 0, // line numbers are required in the format, but not available to reports
],
],
];
}
}
$jsonString = json_encode($report);
return $reportSummary->isDecoratedOutput() ? OutputFormatter::escape($jsonString) : $jsonString;
}
}
@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Console\Report\FixReport;
use Symfony\Component\Console\Formatter\OutputFormatter;
/**
* @author Boris Gorbylev <ekho@ekho.name>
*
* @internal
*/
final class JsonReporter implements ReporterInterface
{
/**
* {@inheritdoc}
*/
public function getFormat(): string
{
return 'json';
}
/**
* {@inheritdoc}
*/
public function generate(ReportSummary $reportSummary): string
{
$jsonFiles = [];
foreach ($reportSummary->getChanged() as $file => $fixResult) {
$jsonFile = ['name' => $file];
if ($reportSummary->shouldAddAppliedFixers()) {
$jsonFile['appliedFixers'] = $fixResult['appliedFixers'];
}
if ('' !== $fixResult['diff']) {
$jsonFile['diff'] = $fixResult['diff'];
}
$jsonFiles[] = $jsonFile;
}
$json = [
'files' => $jsonFiles,
'time' => [
'total' => round($reportSummary->getTime() / 1000, 3),
],
'memory' => round($reportSummary->getMemory() / 1024 / 1024, 3),
];
$json = json_encode($json);
return $reportSummary->isDecoratedOutput() ? OutputFormatter::escape($json) : $json;
}
}
@@ -0,0 +1,141 @@
<?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\Console\Report\FixReport;
use PhpCsFixer\Preg;
use Symfony\Component\Console\Formatter\OutputFormatter;
/**
* @author Boris Gorbylev <ekho@ekho.name>
*
* @internal
*/
final class JunitReporter implements ReporterInterface
{
/**
* {@inheritdoc}
*/
public function getFormat(): string
{
return 'junit';
}
/**
* {@inheritdoc}
*/
public function generate(ReportSummary $reportSummary): string
{
if (!\extension_loaded('dom')) {
throw new \RuntimeException('Cannot generate report! `ext-dom` is not available!');
}
$dom = new \DOMDocument('1.0', 'UTF-8');
$testsuites = $dom->appendChild($dom->createElement('testsuites'));
/** @var \DomElement $testsuite */
$testsuite = $testsuites->appendChild($dom->createElement('testsuite'));
$testsuite->setAttribute('name', 'PHP CS Fixer');
if (\count($reportSummary->getChanged()) > 0) {
$this->createFailedTestCases($dom, $testsuite, $reportSummary);
} else {
$this->createSuccessTestCase($dom, $testsuite);
}
if ($reportSummary->getTime() > 0) {
$testsuite->setAttribute(
'time',
sprintf(
'%.3f',
$reportSummary->getTime() / 1000
)
);
}
$dom->formatOutput = true;
return $reportSummary->isDecoratedOutput() ? OutputFormatter::escape($dom->saveXML()) : $dom->saveXML();
}
private function createSuccessTestCase(\DOMDocument $dom, \DOMElement $testsuite): void
{
$testcase = $dom->createElement('testcase');
$testcase->setAttribute('name', 'All OK');
$testcase->setAttribute('assertions', '1');
$testsuite->appendChild($testcase);
$testsuite->setAttribute('tests', '1');
$testsuite->setAttribute('assertions', '1');
$testsuite->setAttribute('failures', '0');
$testsuite->setAttribute('errors', '0');
}
private function createFailedTestCases(\DOMDocument $dom, \DOMElement $testsuite, ReportSummary $reportSummary): void
{
$assertionsCount = 0;
foreach ($reportSummary->getChanged() as $file => $fixResult) {
$testcase = $this->createFailedTestCase(
$dom,
$file,
$fixResult,
$reportSummary->shouldAddAppliedFixers()
);
$testsuite->appendChild($testcase);
$assertionsCount += (int) $testcase->getAttribute('assertions');
}
$testsuite->setAttribute('tests', (string) \count($reportSummary->getChanged()));
$testsuite->setAttribute('assertions', (string) $assertionsCount);
$testsuite->setAttribute('failures', (string) $assertionsCount);
$testsuite->setAttribute('errors', '0');
}
/**
* @param array{appliedFixers: list<string>, diff: string} $fixResult
*/
private function createFailedTestCase(\DOMDocument $dom, string $file, array $fixResult, bool $shouldAddAppliedFixers): \DOMElement
{
$appliedFixersCount = \count($fixResult['appliedFixers']);
$testName = str_replace('.', '_DOT_', Preg::replace('@\.'.pathinfo($file, PATHINFO_EXTENSION).'$@', '', $file));
$testcase = $dom->createElement('testcase');
$testcase->setAttribute('name', $testName);
$testcase->setAttribute('file', $file);
$testcase->setAttribute('assertions', (string) $appliedFixersCount);
$failure = $dom->createElement('failure');
$failure->setAttribute('type', 'code_style');
$testcase->appendChild($failure);
if ($shouldAddAppliedFixers) {
$failureContent = "applied fixers:\n---------------\n";
foreach ($fixResult['appliedFixers'] as $appliedFixer) {
$failureContent .= "* {$appliedFixer}\n";
}
} else {
$failureContent = "Wrong code style\n";
}
if ('' !== $fixResult['diff']) {
$failureContent .= "\nDiff:\n---------------\n\n".$fixResult['diff'];
}
$failure->appendChild($dom->createCDATASection(trim($failureContent)));
return $testcase;
}
}
@@ -0,0 +1,92 @@
<?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\Console\Report\FixReport;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class ReportSummary
{
/**
* @var array<string, array{appliedFixers: list<string>, diff: string}>
*/
private array $changed;
private int $time;
private int $memory;
private bool $addAppliedFixers;
private bool $isDryRun;
private bool $isDecoratedOutput;
/**
* @param array<string, array{appliedFixers: list<string>, diff: string}> $changed
* @param int $time duration in milliseconds
* @param int $memory memory usage in bytes
*/
public function __construct(
array $changed,
int $time,
int $memory,
bool $addAppliedFixers,
bool $isDryRun,
bool $isDecoratedOutput
) {
$this->changed = $changed;
$this->time = $time;
$this->memory = $memory;
$this->addAppliedFixers = $addAppliedFixers;
$this->isDryRun = $isDryRun;
$this->isDecoratedOutput = $isDecoratedOutput;
}
public function isDecoratedOutput(): bool
{
return $this->isDecoratedOutput;
}
public function isDryRun(): bool
{
return $this->isDryRun;
}
/**
* @return array<string, array{appliedFixers: list<string>, diff: string}>
*/
public function getChanged(): array
{
return $this->changed;
}
public function getMemory(): int
{
return $this->memory;
}
public function getTime(): int
{
return $this->time;
}
public function shouldAddAppliedFixers(): bool
{
return $this->addAppliedFixers;
}
}
@@ -0,0 +1,92 @@
<?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\Console\Report\FixReport;
use Symfony\Component\Finder\Finder as SymfonyFinder;
/**
* @author Boris Gorbylev <ekho@ekho.name>
*
* @internal
*/
final class ReporterFactory
{
/**
* @var array<string, ReporterInterface>
*/
private array $reporters = [];
public function registerBuiltInReporters(): self
{
/** @var null|list<string> $builtInReporters */
static $builtInReporters;
if (null === $builtInReporters) {
$builtInReporters = [];
foreach (SymfonyFinder::create()->files()->name('*Reporter.php')->in(__DIR__) as $file) {
$relativeNamespace = $file->getRelativePath();
$builtInReporters[] = sprintf(
'%s\\%s%s',
__NAMESPACE__,
$relativeNamespace ? $relativeNamespace.'\\' : '',
$file->getBasename('.php')
);
}
}
foreach ($builtInReporters as $reporterClass) {
$this->registerReporter(new $reporterClass());
}
return $this;
}
/**
* @return $this
*/
public function registerReporter(ReporterInterface $reporter): self
{
$format = $reporter->getFormat();
if (isset($this->reporters[$format])) {
throw new \UnexpectedValueException(sprintf('Reporter for format "%s" is already registered.', $format));
}
$this->reporters[$format] = $reporter;
return $this;
}
/**
* @return list<string>
*/
public function getFormats(): array
{
$formats = array_keys($this->reporters);
sort($formats);
return $formats;
}
public function getReporter(string $format): ReporterInterface
{
if (!isset($this->reporters[$format])) {
throw new \UnexpectedValueException(sprintf('Reporter for format "%s" is not registered.', $format));
}
return $this->reporters[$format];
}
}
@@ -0,0 +1,30 @@
<?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\Console\Report\FixReport;
/**
* @author Boris Gorbylev <ekho@ekho.name>
*
* @internal
*/
interface ReporterInterface
{
public function getFormat(): string;
/**
* Process changed files array. Returns generated report.
*/
public function generate(ReportSummary $reportSummary): string;
}
@@ -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\Console\Report\FixReport;
use PhpCsFixer\Differ\DiffConsoleFormatter;
/**
* @author Boris Gorbylev <ekho@ekho.name>
*
* @internal
*/
final class TextReporter implements ReporterInterface
{
/**
* {@inheritdoc}
*/
public function getFormat(): string
{
return 'txt';
}
/**
* {@inheritdoc}
*/
public function generate(ReportSummary $reportSummary): string
{
$output = '';
$i = 0;
foreach ($reportSummary->getChanged() as $file => $fixResult) {
++$i;
$output .= sprintf('%4d) %s', $i, $file);
if ($reportSummary->shouldAddAppliedFixers()) {
$output .= $this->getAppliedFixers(
$reportSummary->isDecoratedOutput(),
$fixResult['appliedFixers'],
);
}
$output .= $this->getDiff($reportSummary->isDecoratedOutput(), $fixResult['diff']);
$output .= PHP_EOL;
}
return $output.$this->getFooter($reportSummary->getTime(), $reportSummary->getMemory(), $reportSummary->isDryRun());
}
/**
* @param list<string> $appliedFixers
*/
private function getAppliedFixers(bool $isDecoratedOutput, array $appliedFixers): string
{
return sprintf(
$isDecoratedOutput ? ' (<comment>%s</comment>)' : ' (%s)',
implode(', ', $appliedFixers)
);
}
private function getDiff(bool $isDecoratedOutput, string $diff): string
{
if ('' === $diff) {
return '';
}
$diffFormatter = new DiffConsoleFormatter($isDecoratedOutput, sprintf(
'<comment> ---------- begin diff ----------</comment>%s%%s%s<comment> ----------- end diff -----------</comment>',
PHP_EOL,
PHP_EOL
));
return PHP_EOL.$diffFormatter->format($diff).PHP_EOL;
}
private function getFooter(int $time, int $memory, bool $isDryRun): string
{
if (0 === $time || 0 === $memory) {
return '';
}
return PHP_EOL.sprintf(
'%s all files in %.3f seconds, %.3f MB memory used'.PHP_EOL,
$isDryRun ? 'Checked' : 'Fixed',
$time / 1000,
$memory / 1024 / 1024
);
}
}
@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Console\Report\FixReport;
use Symfony\Component\Console\Formatter\OutputFormatter;
/**
* @author Boris Gorbylev <ekho@ekho.name>
*
* @internal
*/
final class XmlReporter implements ReporterInterface
{
/**
* {@inheritdoc}
*/
public function getFormat(): string
{
return 'xml';
}
/**
* {@inheritdoc}
*/
public function generate(ReportSummary $reportSummary): string
{
if (!\extension_loaded('dom')) {
throw new \RuntimeException('Cannot generate report! `ext-dom` is not available!');
}
$dom = new \DOMDocument('1.0', 'UTF-8');
// new nodes should be added to this or existing children
$root = $dom->createElement('report');
$dom->appendChild($root);
$filesXML = $dom->createElement('files');
$root->appendChild($filesXML);
$i = 1;
foreach ($reportSummary->getChanged() as $file => $fixResult) {
$fileXML = $dom->createElement('file');
$fileXML->setAttribute('id', (string) $i++);
$fileXML->setAttribute('name', $file);
$filesXML->appendChild($fileXML);
if ($reportSummary->shouldAddAppliedFixers()) {
$fileXML->appendChild(
$this->createAppliedFixersElement($dom, $fixResult['appliedFixers']),
);
}
if ('' !== $fixResult['diff']) {
$fileXML->appendChild($this->createDiffElement($dom, $fixResult['diff']));
}
}
if (0 !== $reportSummary->getTime()) {
$root->appendChild($this->createTimeElement($reportSummary->getTime(), $dom));
}
if (0 !== $reportSummary->getMemory()) {
$root->appendChild($this->createMemoryElement($reportSummary->getMemory(), $dom));
}
$dom->formatOutput = true;
return $reportSummary->isDecoratedOutput() ? OutputFormatter::escape($dom->saveXML()) : $dom->saveXML();
}
/**
* @param list<string> $appliedFixers
*/
private function createAppliedFixersElement(\DOMDocument $dom, array $appliedFixers): \DOMElement
{
$appliedFixersXML = $dom->createElement('applied_fixers');
foreach ($appliedFixers as $appliedFixer) {
$appliedFixerXML = $dom->createElement('applied_fixer');
$appliedFixerXML->setAttribute('name', $appliedFixer);
$appliedFixersXML->appendChild($appliedFixerXML);
}
return $appliedFixersXML;
}
private function createDiffElement(\DOMDocument $dom, string $diff): \DOMElement
{
$diffXML = $dom->createElement('diff');
$diffXML->appendChild($dom->createCDATASection($diff));
return $diffXML;
}
private function createTimeElement(float $time, \DOMDocument $dom): \DOMElement
{
$time = round($time / 1000, 3);
$timeXML = $dom->createElement('time');
$timeXML->setAttribute('unit', 's');
$timeTotalXML = $dom->createElement('total');
$timeTotalXML->setAttribute('value', (string) $time);
$timeXML->appendChild($timeTotalXML);
return $timeXML;
}
private function createMemoryElement(float $memory, \DOMDocument $dom): \DOMElement
{
$memory = round($memory / 1024 / 1024, 3);
$memoryXML = $dom->createElement('memory');
$memoryXML->setAttribute('value', (string) $memory);
$memoryXML->setAttribute('unit', 'MB');
return $memoryXML;
}
}
@@ -0,0 +1,58 @@
<?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\Console\Report\ListSetsReport;
use PhpCsFixer\RuleSet\RuleSetDescriptionInterface;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class JsonReporter implements ReporterInterface
{
/**
* {@inheritdoc}
*/
public function getFormat(): string
{
return 'json';
}
/**
* {@inheritdoc}
*/
public function generate(ReportSummary $reportSummary): string
{
$sets = $reportSummary->getSets();
usort($sets, static function (RuleSetDescriptionInterface $a, RuleSetDescriptionInterface $b): int {
return strcmp($a->getName(), $b->getName());
});
$json = ['sets' => []];
foreach ($sets as $set) {
$setName = $set->getName();
$json['sets'][$setName] = [
'description' => $set->getDescription(),
'isRisky' => $set->isRisky(),
'name' => $setName,
];
}
return json_encode($json, JSON_PRETTY_PRINT);
}
}
@@ -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\Console\Report\ListSetsReport;
use PhpCsFixer\RuleSet\RuleSetDescriptionInterface;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class ReportSummary
{
/**
* @var list<RuleSetDescriptionInterface>
*/
private array $sets;
/**
* @param list<RuleSetDescriptionInterface> $sets
*/
public function __construct(array $sets)
{
$this->sets = $sets;
}
/**
* @return list<RuleSetDescriptionInterface>
*/
public function getSets(): array
{
return $this->sets;
}
}
@@ -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\Console\Report\ListSetsReport;
use Symfony\Component\Finder\Finder as SymfonyFinder;
/**
* @author Boris Gorbylev <ekho@ekho.name>
*
* @internal
*/
final class ReporterFactory
{
/**
* @var array<string, ReporterInterface>
*/
private array $reporters = [];
public function registerBuiltInReporters(): self
{
/** @var null|list<string> $builtInReporters */
static $builtInReporters;
if (null === $builtInReporters) {
$builtInReporters = [];
foreach (SymfonyFinder::create()->files()->name('*Reporter.php')->in(__DIR__) as $file) {
$relativeNamespace = $file->getRelativePath();
$builtInReporters[] = sprintf(
'%s\\%s%s',
__NAMESPACE__,
$relativeNamespace ? $relativeNamespace.'\\' : '',
$file->getBasename('.php')
);
}
}
foreach ($builtInReporters as $reporterClass) {
$this->registerReporter(new $reporterClass());
}
return $this;
}
public function registerReporter(ReporterInterface $reporter): self
{
$format = $reporter->getFormat();
if (isset($this->reporters[$format])) {
throw new \UnexpectedValueException(sprintf('Reporter for format "%s" is already registered.', $format));
}
$this->reporters[$format] = $reporter;
return $this;
}
/**
* @return list<string>
*/
public function getFormats(): array
{
$formats = array_keys($this->reporters);
sort($formats);
return $formats;
}
public function getReporter(string $format): ReporterInterface
{
if (!isset($this->reporters[$format])) {
throw new \UnexpectedValueException(sprintf('Reporter for format "%s" is not registered.', $format));
}
return $this->reporters[$format];
}
}
@@ -0,0 +1,30 @@
<?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\Console\Report\ListSetsReport;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
interface ReporterInterface
{
public function getFormat(): string;
/**
* Process changed files array. Returns generated report.
*/
public function generate(ReportSummary $reportSummary): string;
}
@@ -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\Console\Report\ListSetsReport;
use PhpCsFixer\RuleSet\RuleSetDescriptionInterface;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class TextReporter implements ReporterInterface
{
/**
* {@inheritdoc}
*/
public function getFormat(): string
{
return 'txt';
}
/**
* {@inheritdoc}
*/
public function generate(ReportSummary $reportSummary): string
{
$sets = $reportSummary->getSets();
usort($sets, static function (RuleSetDescriptionInterface $a, RuleSetDescriptionInterface $b): int {
return strcmp($a->getName(), $b->getName());
});
$output = '';
foreach ($sets as $i => $set) {
$output .= sprintf('%2d) %s', $i + 1, $set->getName()).PHP_EOL.' '.$set->getDescription().PHP_EOL;
if ($set->isRisky()) {
$output .= ' Set contains risky rules.'.PHP_EOL;
}
}
return $output;
}
}
@@ -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\Console\SelfUpdate;
/**
* @internal
*/
final class GithubClient implements GithubClientInterface
{
/**
* {@inheritdoc}
*/
public function getTags(): array
{
$url = 'https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/tags';
$result = @file_get_contents(
$url,
false,
stream_context_create([
'http' => [
'header' => 'User-Agent: PHP-CS-Fixer/PHP-CS-Fixer',
],
])
);
if (false === $result) {
throw new \RuntimeException(sprintf('Failed to load tags at "%s".', $url));
}
$result = json_decode($result, true);
if (JSON_ERROR_NONE !== json_last_error()) {
throw new \RuntimeException(sprintf(
'Failed to read response from "%s" as JSON: %s.',
$url,
json_last_error_msg()
));
}
return $result;
}
}
@@ -0,0 +1,31 @@
<?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\Console\SelfUpdate;
/**
* @internal
*/
interface GithubClientInterface
{
/**
* @return list<array{
* name: string,
* zipball_url: string,
* tarball_url: string,
* commit: array{sha: string, url: string},
* }>
*/
public function getTags(): array;
}
@@ -0,0 +1,110 @@
<?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\Console\SelfUpdate;
use Composer\Semver\Comparator;
use Composer\Semver\Semver;
use Composer\Semver\VersionParser;
/**
* @internal
*/
final class NewVersionChecker implements NewVersionCheckerInterface
{
private GithubClientInterface $githubClient;
private VersionParser $versionParser;
/**
* @var null|string[]
*/
private $availableVersions;
public function __construct(GithubClientInterface $githubClient)
{
$this->githubClient = $githubClient;
$this->versionParser = new VersionParser();
}
/**
* {@inheritdoc}
*/
public function getLatestVersion(): string
{
$this->retrieveAvailableVersions();
return $this->availableVersions[0];
}
/**
* {@inheritdoc}
*/
public function getLatestVersionOfMajor(int $majorVersion): ?string
{
$this->retrieveAvailableVersions();
$semverConstraint = '^'.$majorVersion;
foreach ($this->availableVersions as $availableVersion) {
if (Semver::satisfies($availableVersion, $semverConstraint)) {
return $availableVersion;
}
}
return null;
}
/**
* {@inheritdoc}
*/
public function compareVersions(string $versionA, string $versionB): int
{
$versionA = $this->versionParser->normalize($versionA);
$versionB = $this->versionParser->normalize($versionB);
if (Comparator::lessThan($versionA, $versionB)) {
return -1;
}
if (Comparator::greaterThan($versionA, $versionB)) {
return 1;
}
return 0;
}
private function retrieveAvailableVersions(): void
{
if (null !== $this->availableVersions) {
return;
}
foreach ($this->githubClient->getTags() as $tag) {
$version = $tag['name'];
try {
$this->versionParser->normalize($version);
if ('stable' === VersionParser::parseStability($version)) {
$this->availableVersions[] = $version;
}
} catch (\UnexpectedValueException $exception) {
// not a valid version tag
}
}
$this->availableVersions = Semver::rsort($this->availableVersions);
}
}
@@ -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\Console\SelfUpdate;
/**
* @internal
*/
interface NewVersionCheckerInterface
{
/**
* Returns the tag of the latest version.
*/
public function getLatestVersion(): string;
/**
* Returns the tag of the latest minor/patch version of the given major version.
*/
public function getLatestVersionOfMajor(int $majorVersion): ?string;
/**
* Returns -1, 0, or 1 if the first version is respectively less than,
* equal to, or greater than the second.
*/
public function compareVersions(string $versionA, string $versionB): int;
}
@@ -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\Console;
use PhpCsFixer\ToolInfo;
use PhpCsFixer\ToolInfoInterface;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class WarningsDetector
{
private ToolInfoInterface $toolInfo;
/**
* @var string[]
*/
private array $warnings = [];
public function __construct(ToolInfoInterface $toolInfo)
{
$this->toolInfo = $toolInfo;
}
public function detectOldMajor(): void
{
// @TODO 3.99 to be activated with new MAJOR release 4.0
// $currentMajorVersion = \intval(explode('.', Application::VERSION)[0], 10);
// $nextMajorVersion = $currentMajorVersion + 1;
// $this->warnings[] = "You are running PHP CS Fixer v{$currentMajorVersion}, which is not maintained anymore. Please update to v{$nextMajorVersion}.";
// $this->warnings[] = "You may find an UPGRADE guide at https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/v{$nextMajorVersion}.0.0/UPGRADE-v{$nextMajorVersion}.md .";
}
public function detectOldVendor(): void
{
if ($this->toolInfo->isInstalledByComposer()) {
$details = $this->toolInfo->getComposerInstallationDetails();
if (ToolInfo::COMPOSER_LEGACY_PACKAGE_NAME === $details['name']) {
$this->warnings[] = sprintf(
'You are running PHP CS Fixer installed with old vendor `%s`. Please update to `%s`.',
ToolInfo::COMPOSER_LEGACY_PACKAGE_NAME,
ToolInfo::COMPOSER_PACKAGE_NAME
);
}
}
}
/**
* @return string[]
*/
public function getWarnings(): array
{
if (0 === \count($this->warnings)) {
return [];
}
return array_unique(array_merge(
$this->warnings,
['If you need help while solving warnings, ask at https://gitter.im/PHP-CS-Fixer, we will help you!']
));
}
}
@@ -0,0 +1,84 @@
<?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\Differ;
use PhpCsFixer\Preg;
use Symfony\Component\Console\Formatter\OutputFormatter;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class DiffConsoleFormatter
{
private bool $isDecoratedOutput;
private string $template;
public function __construct(bool $isDecoratedOutput, string $template = '%s')
{
$this->isDecoratedOutput = $isDecoratedOutput;
$this->template = $template;
}
public function format(string $diff, string $lineTemplate = '%s'): string
{
$isDecorated = $this->isDecoratedOutput;
$template = $isDecorated
? $this->template
: Preg::replace('/<[^<>]+>/', '', $this->template)
;
return sprintf(
$template,
implode(
PHP_EOL,
array_map(
static function (string $line) use ($isDecorated, $lineTemplate): string {
if ($isDecorated) {
$count = 0;
$line = Preg::replaceCallback(
'/^([+\-@].*)/',
static function (array $matches): string {
if ('+' === $matches[0][0]) {
$colour = 'green';
} elseif ('-' === $matches[0][0]) {
$colour = 'red';
} else {
$colour = 'cyan';
}
return sprintf('<fg=%s>%s</fg=%s>', $colour, OutputFormatter::escape($matches[0]), $colour);
},
$line,
1,
$count
);
if (0 === $count) {
$line = OutputFormatter::escape($line);
}
}
return sprintf($lineTemplate, $line);
},
Preg::split('#\R#u', $diff)
)
)
);
}
}
@@ -0,0 +1,26 @@
<?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\Differ;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
interface DifferInterface
{
/**
* Create diff.
*/
public function diff(string $old, string $new, ?\SplFileInfo $file = null): string;
}
@@ -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\Differ;
use SebastianBergmann\Diff\Differ;
use SebastianBergmann\Diff\Output\StrictUnifiedDiffOutputBuilder;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class FullDiffer implements DifferInterface
{
private Differ $differ;
public function __construct()
{
$this->differ = new Differ(new StrictUnifiedDiffOutputBuilder([
'collapseRanges' => false,
'commonLineThreshold' => 100,
'contextLines' => 100,
'fromFile' => 'Original',
'toFile' => 'New',
]));
}
/**
* {@inheritdoc}
*/
public function diff(string $old, string $new, ?\SplFileInfo $file = null): string
{
return $this->differ->diff($old, $new);
}
}
@@ -0,0 +1,29 @@
<?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\Differ;
/**
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class NullDiffer implements DifferInterface
{
/**
* {@inheritdoc}
*/
public function diff(string $old, string $new, ?\SplFileInfo $file = null): string
{
return '';
}
}
@@ -0,0 +1,50 @@
<?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\Differ;
use PhpCsFixer\Preg;
use SebastianBergmann\Diff\Differ;
use SebastianBergmann\Diff\Output\StrictUnifiedDiffOutputBuilder;
final class UnifiedDiffer implements DifferInterface
{
/**
* {@inheritdoc}
*/
public function diff(string $old, string $new, ?\SplFileInfo $file = null): string
{
if (null === $file) {
$options = [
'fromFile' => 'Original',
'toFile' => 'New',
];
} else {
$filePath = $file->getRealPath();
if (1 === Preg::match('/\s/', $filePath)) {
$filePath = '"'.$filePath.'"';
}
$options = [
'fromFile' => $filePath,
'toFile' => $filePath,
];
}
$differ = new Differ(new StrictUnifiedDiffOutputBuilder($options));
return $differ->diff($old, $new);
}
}
@@ -0,0 +1,306 @@
<?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\DocBlock;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
/**
* This represents an entire annotation from a docblock.
*
* @author Graham Campbell <hello@gjcampbell.co.uk>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class Annotation
{
/**
* All the annotation tag names with types.
*
* @var string[]
*/
private static array $tags = [
'method',
'param',
'property',
'property-read',
'property-write',
'return',
'throws',
'type',
'var',
];
/**
* The lines that make up the annotation.
*
* @var Line[]
*/
private array $lines;
/**
* The position of the first line of the annotation in the docblock.
*
* @var int
*/
private $start;
/**
* The position of the last line of the annotation in the docblock.
*
* @var int
*/
private $end;
/**
* The associated tag.
*
* @var null|Tag
*/
private $tag;
/**
* Lazy loaded, cached types content.
*
* @var null|string
*/
private $typesContent;
/**
* The cached types.
*
* @var null|string[]
*/
private $types;
/**
* @var null|NamespaceAnalysis
*/
private $namespace;
/**
* @var NamespaceUseAnalysis[]
*/
private array $namespaceUses;
/**
* Create a new line instance.
*
* @param Line[] $lines
* @param null|NamespaceAnalysis $namespace
* @param NamespaceUseAnalysis[] $namespaceUses
*/
public function __construct(array $lines, $namespace = null, array $namespaceUses = [])
{
$this->lines = array_values($lines);
$this->namespace = $namespace;
$this->namespaceUses = $namespaceUses;
$keys = array_keys($lines);
$this->start = $keys[0];
$this->end = end($keys);
}
/**
* Get the string representation of object.
*/
public function __toString(): string
{
return $this->getContent();
}
/**
* Get all the annotation tag names with types.
*
* @return string[]
*/
public static function getTagsWithTypes(): array
{
return self::$tags;
}
/**
* Get the start position of this annotation.
*/
public function getStart(): int
{
return $this->start;
}
/**
* Get the end position of this annotation.
*/
public function getEnd(): int
{
return $this->end;
}
/**
* Get the associated tag.
*/
public function getTag(): Tag
{
if (null === $this->tag) {
$this->tag = new Tag($this->lines[0]);
}
return $this->tag;
}
/**
* @internal
*/
public function getTypeExpression(): TypeExpression
{
return new TypeExpression($this->getTypesContent(), $this->namespace, $this->namespaceUses);
}
/**
* @return null|string
*
* @internal
*/
public function getVariableName()
{
$type = preg_quote($this->getTypesContent(), '/');
$regex = "/@{$this->tag->getName()}\\s+({$type}\\s*)?(&\\s*)?(\\.{3}\\s*)?(?<variable>\\$.+?)(?:[\\s*]|$)/";
if (Preg::match($regex, $this->lines[0]->getContent(), $matches)) {
return $matches['variable'];
}
return null;
}
/**
* Get the types associated with this annotation.
*
* @return string[]
*/
public function getTypes(): array
{
if (null === $this->types) {
$this->types = $this->getTypeExpression()->getTypes();
}
return $this->types;
}
/**
* Set the types associated with this annotation.
*
* @param string[] $types
*/
public function setTypes(array $types): void
{
$pattern = '/'.preg_quote($this->getTypesContent(), '/').'/';
$this->lines[0]->setContent(Preg::replace($pattern, implode($this->getTypeExpression()->getTypesGlue(), $types), $this->lines[0]->getContent(), 1));
$this->clearCache();
}
/**
* Get the normalized types associated with this annotation, so they can easily be compared.
*
* @return string[]
*/
public function getNormalizedTypes(): array
{
$normalized = array_map(static function (string $type): string {
return strtolower($type);
}, $this->getTypes());
sort($normalized);
return $normalized;
}
/**
* Remove this annotation by removing all its lines.
*/
public function remove(): void
{
foreach ($this->lines as $line) {
if ($line->isTheStart() && $line->isTheEnd()) {
// Single line doc block, remove entirely
$line->remove();
} elseif ($line->isTheStart()) {
// Multi line doc block, but start is on the same line as the first annotation, keep only the start
$content = Preg::replace('#(\s*/\*\*).*#', '$1', $line->getContent());
$line->setContent($content);
} elseif ($line->isTheEnd()) {
// Multi line doc block, but end is on the same line as the last annotation, keep only the end
$content = Preg::replace('#(\s*)\S.*(\*/.*)#', '$1$2', $line->getContent());
$line->setContent($content);
} else {
// Multi line doc block, neither start nor end on this line, can be removed safely
$line->remove();
}
}
$this->clearCache();
}
/**
* Get the annotation content.
*/
public function getContent(): string
{
return implode('', $this->lines);
}
public function supportTypes(): bool
{
return \in_array($this->getTag()->getName(), self::$tags, true);
}
/**
* Get the current types content.
*
* Be careful modifying the underlying line as that won't flush the cache.
*/
private function getTypesContent(): string
{
if (null === $this->typesContent) {
$name = $this->getTag()->getName();
if (!$this->supportTypes()) {
throw new \RuntimeException('This tag does not support types.');
}
$matchingResult = Preg::match(
'{^(?:\s*\*|/\*\*)\s*@'.$name.'\s+'.TypeExpression::REGEX_TYPES.'(?:(?:[*\h\v]|\&[\.\$]).*)?\r?$}isx',
$this->lines[0]->getContent(),
$matches
);
$this->typesContent = 1 === $matchingResult
? $matches['types']
: '';
}
return $this->typesContent;
}
private function clearCache(): void
{
$this->types = null;
$this->typesContent = null;
}
}
@@ -0,0 +1,252 @@
<?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\DocBlock;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
/**
* This class represents a docblock.
*
* It internally splits it up into "lines" that we can manipulate.
*
* @author Graham Campbell <hello@gjcampbell.co.uk>
*/
final class DocBlock
{
/**
* @var list<Line>
*/
private array $lines = [];
/**
* @var null|list<Annotation>
*/
private ?array $annotations = null;
private ?NamespaceAnalysis $namespace;
/**
* @var list<NamespaceUseAnalysis>
*/
private array $namespaceUses;
/**
* @param list<NamespaceUseAnalysis> $namespaceUses
*/
public function __construct(string $content, ?NamespaceAnalysis $namespace = null, array $namespaceUses = [])
{
foreach (Preg::split('/([^\n\r]+\R*)/', $content, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $line) {
$this->lines[] = new Line($line);
}
$this->namespace = $namespace;
$this->namespaceUses = $namespaceUses;
}
public function __toString(): string
{
return $this->getContent();
}
/**
* Get this docblock's lines.
*
* @return list<Line>
*/
public function getLines(): array
{
return $this->lines;
}
/**
* Get a single line.
*/
public function getLine(int $pos): ?Line
{
return $this->lines[$pos] ?? null;
}
/**
* Get this docblock's annotations.
*
* @return list<Annotation>
*/
public function getAnnotations(): array
{
if (null !== $this->annotations) {
return $this->annotations;
}
$this->annotations = [];
$total = \count($this->lines);
for ($index = 0; $index < $total; ++$index) {
if ($this->lines[$index]->containsATag()) {
// get all the lines that make up the annotation
$lines = \array_slice($this->lines, $index, $this->findAnnotationLength($index), true);
$annotation = new Annotation($lines, $this->namespace, $this->namespaceUses);
// move the index to the end of the annotation to avoid
// checking it again because we know the lines inside the
// current annotation cannot be part of another annotation
$index = $annotation->getEnd();
// add the current annotation to the list of annotations
$this->annotations[] = $annotation;
}
}
return $this->annotations;
}
public function isMultiLine(): bool
{
return 1 !== \count($this->lines);
}
/**
* Take a one line doc block, and turn it into a multi line doc block.
*/
public function makeMultiLine(string $indent, string $lineEnd): void
{
if ($this->isMultiLine()) {
return;
}
$lineContent = $this->getSingleLineDocBlockEntry($this->lines[0]);
if ('' === $lineContent) {
$this->lines = [
new Line('/**'.$lineEnd),
new Line($indent.' *'.$lineEnd),
new Line($indent.' */'),
];
return;
}
$this->lines = [
new Line('/**'.$lineEnd),
new Line($indent.' * '.$lineContent.$lineEnd),
new Line($indent.' */'),
];
}
public function makeSingleLine(): void
{
if (!$this->isMultiLine()) {
return;
}
$usefulLines = array_filter(
$this->lines,
static function (Line $line): bool {
return $line->containsUsefulContent();
}
);
if (1 < \count($usefulLines)) {
return;
}
$lineContent = '';
if (\count($usefulLines) > 0) {
$lineContent = $this->getSingleLineDocBlockEntry(array_shift($usefulLines));
}
$this->lines = [new Line('/** '.$lineContent.' */')];
}
public function getAnnotation(int $pos): ?Annotation
{
$annotations = $this->getAnnotations();
return $annotations[$pos] ?? null;
}
/**
* Get specific types of annotations only.
*
* @param list<string>|string $types
*
* @return list<Annotation>
*/
public function getAnnotationsOfType($types): array
{
$typesToSearchFor = (array) $types;
$annotations = [];
foreach ($this->getAnnotations() as $annotation) {
$tagName = $annotation->getTag()->getName();
if (\in_array($tagName, $typesToSearchFor, true)) {
$annotations[] = $annotation;
}
}
return $annotations;
}
/**
* Get the actual content of this docblock.
*/
public function getContent(): string
{
return implode('', $this->lines);
}
private function findAnnotationLength(int $start): int
{
$index = $start;
while ($line = $this->getLine(++$index)) {
if ($line->containsATag()) {
// we've 100% reached the end of the description if we get here
break;
}
if (!$line->containsUsefulContent()) {
// if next line is also non-useful, or contains a tag, then we're done here
$next = $this->getLine($index + 1);
if (null === $next || !$next->containsUsefulContent() || $next->containsATag()) {
break;
}
// otherwise, continue, the annotation must have contained a blank line in its description
}
}
return $index - $start;
}
private function getSingleLineDocBlockEntry(Line $line): string
{
$lineString = $line->getContent();
if ('' === $lineString) {
return $lineString;
}
$lineString = str_replace('*/', '', $lineString);
$lineString = trim($lineString);
if (str_starts_with($lineString, '/**')) {
$lineString = substr($lineString, 3);
} elseif (str_starts_with($lineString, '*')) {
$lineString = substr($lineString, 1);
}
return trim($lineString);
}
}
@@ -0,0 +1,128 @@
<?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\DocBlock;
use PhpCsFixer\Preg;
/**
* This represents a line of a docblock.
*
* @author Graham Campbell <hello@gjcampbell.co.uk>
*/
final class Line
{
/**
* The content of this line.
*/
private string $content;
/**
* Create a new line instance.
*/
public function __construct(string $content)
{
$this->content = $content;
}
/**
* Get the string representation of object.
*/
public function __toString(): string
{
return $this->content;
}
/**
* Get the content of this line.
*/
public function getContent(): string
{
return $this->content;
}
/**
* Does this line contain useful content?
*
* If the line contains text or tags, then this is true.
*/
public function containsUsefulContent(): bool
{
return 0 !== Preg::match('/\\*\s*\S+/', $this->content) && '' !== trim(str_replace(['/', '*'], ' ', $this->content));
}
/**
* Does the line contain a tag?
*
* If this is true, then it must be the first line of an annotation.
*/
public function containsATag(): bool
{
return 0 !== Preg::match('/\\*\s*@/', $this->content);
}
/**
* Is the line the start of a docblock?
*/
public function isTheStart(): bool
{
return str_contains($this->content, '/**');
}
/**
* Is the line the end of a docblock?
*/
public function isTheEnd(): bool
{
return str_contains($this->content, '*/');
}
/**
* Set the content of this line.
*/
public function setContent(string $content): void
{
$this->content = $content;
}
/**
* Remove this line by clearing its contents.
*
* Note that this method technically brakes the internal state of the
* docblock, but is useful when we need to retain the indices of lines
* during the execution of an algorithm.
*/
public function remove(): void
{
$this->content = '';
}
/**
* Append a blank docblock line to this line's contents.
*
* Note that this method technically brakes the internal state of the
* docblock, but is useful when we need to retain the indices of lines
* during the execution of an algorithm.
*/
public function addBlank(): void
{
$matched = Preg::match('/^(\h*\*)[^\r\n]*(\r?\n)$/', $this->content, $matches);
if (1 !== $matched) {
return;
}
$this->content .= $matches[1].$matches[2];
}
}
@@ -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\DocBlock;
/**
* This class represents a short description (aka summary) of a docblock.
*
* @internal
*/
final class ShortDescription
{
/**
* The docblock containing the short description.
*/
private DocBlock $doc;
public function __construct(DocBlock $doc)
{
$this->doc = $doc;
}
/**
* Get the line index of the line containing the end of the short
* description, if present.
*/
public function getEnd(): ?int
{
$reachedContent = false;
foreach ($this->doc->getLines() as $index => $line) {
// we went past a description, then hit a tag or blank line, so
// the last line of the description must be the one before this one
if ($reachedContent && ($line->containsATag() || !$line->containsUsefulContent())) {
return $index - 1;
}
// no short description was found
if ($line->containsATag()) {
return null;
}
// we've reached content, but need to check the next lines too
// in case the short description is multi-line
if ($line->containsUsefulContent()) {
$reachedContent = true;
}
}
return null;
}
}
@@ -0,0 +1,102 @@
<?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\DocBlock;
use PhpCsFixer\Preg;
/**
* This represents a tag, as defined by the proposed PSR PHPDoc standard.
*
* @author Graham Campbell <hello@gjcampbell.co.uk>
* @author Jakub Kwaśniewski <jakub@zero-85.pl>
*/
final class Tag
{
/**
* All the tags defined by the proposed PSR PHPDoc standard.
*/
public const PSR_STANDARD_TAGS = [
'api', 'author', 'category', 'copyright', 'deprecated', 'example',
'global', 'internal', 'license', 'link', 'method', 'package', 'param',
'property', 'property-read', 'property-write', 'return', 'see',
'since', 'subpackage', 'throws', 'todo', 'uses', 'var', 'version',
];
/**
* The line containing the tag.
*/
private Line $line;
/**
* The cached tag name.
*/
private ?string $name = null;
/**
* Create a new tag instance.
*/
public function __construct(Line $line)
{
$this->line = $line;
}
/**
* Get the tag name.
*
* This may be "param", or "return", etc.
*/
public function getName(): string
{
if (null === $this->name) {
Preg::matchAll('/@[a-zA-Z0-9_-]+(?=\s|$)/', $this->line->getContent(), $matches);
if (isset($matches[0][0])) {
$this->name = ltrim($matches[0][0], '@');
} else {
$this->name = 'other';
}
}
return $this->name;
}
/**
* Set the tag name.
*
* This will also be persisted to the upstream line and annotation.
*/
public function setName(string $name): void
{
$current = $this->getName();
if ('other' === $current) {
throw new \RuntimeException('Cannot set name on unknown tag.');
}
$this->line->setContent(Preg::replace("/@{$current}/", "@{$name}", $this->line->getContent(), 1));
$this->name = $name;
}
/**
* Is the tag a known tag?
*
* This is defined by if it exists in the proposed PSR PHPDoc standard.
*/
public function valid(): bool
{
return \in_array($this->getName(), self::PSR_STANDARD_TAGS, true);
}
}
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\DocBlock;
/**
* This class is responsible for comparing tags to see if they should be kept
* together, or kept apart.
*
* @author Graham Campbell <hello@gjcampbell.co.uk>
* @author Jakub Kwaśniewski <jakub@zero-85.pl>
*/
final class TagComparator
{
/**
* Groups of tags that should be allowed to immediately follow each other.
*
* @internal
*/
public const DEFAULT_GROUPS = [
['deprecated', 'link', 'see', 'since'],
['author', 'copyright', 'license'],
['category', 'package', 'subpackage'],
['property', 'property-read', 'property-write'],
];
/**
* Should the given tags be kept together, or kept apart?
*
* @param string[][] $groups
*/
public static function shouldBeTogether(Tag $first, Tag $second, array $groups = self::DEFAULT_GROUPS): bool
{
$firstName = $first->getName();
$secondName = $second->getName();
if ($firstName === $secondName) {
return true;
}
foreach ($groups as $group) {
if (\in_array($firstName, $group, true) && \in_array($secondName, $group, true)) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,465 @@
<?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\DocBlock;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
use PhpCsFixer\Utils;
/**
* @internal
*/
final class TypeExpression
{
/**
* Regex to match any types, shall be used with `x` modifier.
*
* @internal
*/
public const REGEX_TYPES = '
(?<types> # several types separated by `|` or `&`
(?<type> # single type
(?<nullable>\??)
(?:
(?<object_like_array>
(?<object_like_array_start>array\h*\{)
(?<object_like_array_keys>
(?<object_like_array_key>
\h*[^?:\h]+\h*\??\h*:\h*(?&types)
)
(?:\h*,(?&object_like_array_key))*
)
\h*\}
)
|
(?<callable> # callable syntax, e.g. `callable(string): bool`
(?<callable_start>(?:callable|Closure)\h*\(\h*)
(?<callable_arguments>
(?&types)
(?:
\h*,\h*
(?&types)
)*
)?
\h*\)
(?:
\h*\:\h*
(?<callable_return>(?&types))
)?
)
|
(?<generic> # generic syntax, e.g.: `array<int, \Foo\Bar>`
(?<generic_start>
(?&name)+
\h*<\h*
)
(?<generic_types>
(?&types)
(?:
\h*,\h*
(?&types)
)*
)
\h*>
)
|
(?<class_constant> # class constants with optional wildcard, e.g.: `Foo::*`, `Foo::CONST_A`, `FOO::CONST_*`
(?&name)::(\*|\w+\*?)
)
|
(?<array> # array expression, e.g.: `string[]`, `string[][]`
(?&name)(\[\])+
)
|
(?<constant> # single constant value (case insensitive), e.g.: 1, `\'a\'`
(?i)
null | true | false
| -?(?:\d+(?:\.\d*)?|\.\d+) # all sorts of numbers with or without minus, e.g.: 1, 1.1, 1., .1, -1
| \'[^\']+?\' | "[^"]+?"
| [@$]?(?:this | self | static)
(?-i)
)
|
(?<name> # single type, e.g.: `null`, `int`, `\Foo\Bar`
[\\\\\w-]++
)
)
)
(?:
\h*(?<glue>[|&])\h*
(?&type)
)*
)
';
private string $value;
private bool $isUnionType = false;
/**
* @var list<array{start_index: int, expression: self}>
*/
private array $innerTypeExpressions = [];
private string $typesGlue = '|';
private ?NamespaceAnalysis $namespace;
/**
* @var NamespaceUseAnalysis[]
*/
private array $namespaceUses;
/**
* @param NamespaceUseAnalysis[] $namespaceUses
*/
public function __construct(string $value, ?NamespaceAnalysis $namespace, array $namespaceUses)
{
$this->value = $value;
$this->namespace = $namespace;
$this->namespaceUses = $namespaceUses;
$this->parse();
}
public function toString(): string
{
return $this->value;
}
/**
* @return string[]
*/
public function getTypes(): array
{
if ($this->isUnionType) {
return array_map(
static fn (array $type) => $type['expression']->toString(),
$this->innerTypeExpressions,
);
}
return [$this->value];
}
/**
* @param callable(self $a, self $b): int $compareCallback
*/
public function sortTypes(callable $compareCallback): void
{
foreach (array_reverse($this->innerTypeExpressions) as [
'start_index' => $startIndex,
'expression' => $inner,
]) {
$initialValueLength = \strlen($inner->toString());
$inner->sortTypes($compareCallback);
$this->value = substr_replace(
$this->value,
$inner->toString(),
$startIndex,
$initialValueLength
);
}
if ($this->isUnionType) {
$this->innerTypeExpressions = Utils::stableSort(
$this->innerTypeExpressions,
static fn (array $type): self => $type['expression'],
$compareCallback,
);
$this->value = implode($this->getTypesGlue(), $this->getTypes());
}
}
public function getTypesGlue(): string
{
return $this->typesGlue;
}
public function getCommonType(): ?string
{
$aliases = $this->getAliases();
$mainType = null;
foreach ($this->getTypes() as $type) {
if ('null' === $type) {
continue;
}
if (isset($aliases[$type])) {
$type = $aliases[$type];
} elseif (1 === Preg::match('/\[\]$/', $type)) {
$type = 'array';
} elseif (1 === Preg::match('/^(.+?)</', $type, $matches)) {
$type = $matches[1];
}
if (null === $mainType || $type === $mainType) {
$mainType = $type;
continue;
}
$mainType = $this->getParentType($type, $mainType);
if (null === $mainType) {
return null;
}
}
return $mainType;
}
public function allowsNull(): bool
{
foreach ($this->getTypes() as $type) {
if (\in_array($type, ['null', 'mixed'], true)) {
return true;
}
}
return false;
}
private function parse(): void
{
$value = $this->value;
Preg::match(
'{^'.self::REGEX_TYPES.'$}x',
$value,
$matches
);
if ([] === $matches) {
return;
}
$this->typesGlue = $matches['glue'] ?? $this->typesGlue;
$index = '' !== $matches['nullable'] ? 1 : 0;
if ($matches['type'] !== $matches['types']) {
$this->isUnionType = true;
while (true) {
$innerType = $matches['type'];
$newValue = Preg::replace(
'/^'.preg_quote($innerType, '/').'(\h*[|&]\h*)?/',
'',
$value
);
$this->innerTypeExpressions[] = [
'start_index' => $index,
'expression' => $this->inner($innerType),
];
if ('' === $newValue) {
return;
}
$index += \strlen($value) - \strlen($newValue);
$value = $newValue;
Preg::match(
'{^'.self::REGEX_TYPES.'$}x',
$value,
$matches
);
}
}
if ('' !== ($matches['generic'] ?? '')) {
$this->parseCommaSeparatedInnerTypes(
$index + \strlen($matches['generic_start']),
$matches['generic_types']
);
return;
}
if ('' !== ($matches['callable'] ?? '')) {
$this->parseCommaSeparatedInnerTypes(
$index + \strlen($matches['callable_start']),
$matches['callable_arguments'] ?? ''
);
$return = $matches['callable_return'] ?? null;
if (null !== $return) {
$this->innerTypeExpressions[] = [
'start_index' => \strlen($this->value) - \strlen($matches['callable_return']),
'expression' => $this->inner($matches['callable_return']),
];
}
return;
}
if ('' !== ($matches['object_like_array'] ?? '')) {
$this->parseObjectLikeArrayKeys(
$index + \strlen($matches['object_like_array_start']),
$matches['object_like_array_keys']
);
}
}
private function parseCommaSeparatedInnerTypes(int $startIndex, string $value): void
{
while ('' !== $value) {
Preg::match(
'{^'.self::REGEX_TYPES.'\h*(?:,|$)}x',
$value,
$matches
);
$this->innerTypeExpressions[] = [
'start_index' => $startIndex,
'expression' => $this->inner($matches['types']),
];
$newValue = Preg::replace(
'/^'.preg_quote($matches['types'], '/').'(\h*\,\h*)?/',
'',
$value
);
$startIndex += \strlen($value) - \strlen($newValue);
$value = $newValue;
}
}
private function parseObjectLikeArrayKeys(int $startIndex, string $value): void
{
while ('' !== $value) {
Preg::match(
'{(?<_start>^.+?:\h*)'.self::REGEX_TYPES.'\h*(?:,|$)}x',
$value,
$matches
);
$this->innerTypeExpressions[] = [
'start_index' => $startIndex + \strlen($matches['_start']),
'expression' => $this->inner($matches['types']),
];
$newValue = Preg::replace(
'/^.+?:\h*'.preg_quote($matches['types'], '/').'(\h*\,\h*)?/',
'',
$value
);
$startIndex += \strlen($value) - \strlen($newValue);
$value = $newValue;
}
}
private function inner(string $value): self
{
return new self($value, $this->namespace, $this->namespaceUses);
}
private function getParentType(string $type1, string $type2): ?string
{
$types = [
$this->normalize($type1),
$this->normalize($type2),
];
natcasesort($types);
$types = implode('|', $types);
$parents = [
'array|Traversable' => 'iterable',
'array|iterable' => 'iterable',
'iterable|Traversable' => 'iterable',
'self|static' => 'self',
];
return $parents[$types] ?? null;
}
private function normalize(string $type): string
{
$aliases = $this->getAliases();
if (isset($aliases[$type])) {
return $aliases[$type];
}
if (\in_array($type, [
'array',
'bool',
'callable',
'float',
'int',
'iterable',
'mixed',
'never',
'null',
'object',
'resource',
'string',
'void',
], true)) {
return $type;
}
if (1 === Preg::match('/\[\]$/', $type)) {
return 'array';
}
if (1 === Preg::match('/^(.+?)</', $type, $matches)) {
return $matches[1];
}
if (str_starts_with($type, '\\')) {
return substr($type, 1);
}
foreach ($this->namespaceUses as $namespaceUse) {
if ($namespaceUse->getShortName() === $type) {
return $namespaceUse->getFullName();
}
}
if (null === $this->namespace || $this->namespace->isGlobalNamespace()) {
return $type;
}
return "{$this->namespace->getFullName()}\\{$type}";
}
/**
* @return array<string,string>
*/
private function getAliases(): array
{
return [
'boolean' => 'bool',
'callback' => 'callable',
'double' => 'float',
'false' => 'bool',
'integer' => 'int',
'real' => 'float',
'true' => 'bool',
];
}
}
@@ -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\Doctrine\Annotation;
use Doctrine\Common\Annotations\DocLexer;
/**
* A Doctrine annotation token.
*
* @internal
*/
final class Token
{
private int $type;
private string $content;
/**
* @param int $type The type
* @param string $content The content
*/
public function __construct(int $type = DocLexer::T_NONE, string $content = '')
{
$this->type = $type;
$this->content = $content;
}
public function getType(): int
{
return $this->type;
}
public function setType(int $type): void
{
$this->type = $type;
}
public function getContent(): string
{
return $this->content;
}
public function setContent(string $content): void
{
$this->content = $content;
}
/**
* Returns whether the token type is one of the given types.
*
* @param int|int[] $types
*/
public function isType($types): bool
{
if (!\is_array($types)) {
$types = [$types];
}
return \in_array($this->getType(), $types, true);
}
/**
* Overrides the content with an empty string.
*/
public function clear(): void
{
$this->setContent('');
}
}
@@ -0,0 +1,302 @@
<?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\Doctrine\Annotation;
use Doctrine\Common\Annotations\DocLexer;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token as PhpToken;
/**
* A list of Doctrine annotation tokens.
*
* @internal
*
* @extends \SplFixedArray<Token>
*/
final class Tokens extends \SplFixedArray
{
/**
* @param string[] $ignoredTags
*
* @throws \InvalidArgumentException
*/
public static function createFromDocComment(PhpToken $input, array $ignoredTags = []): self
{
if (!$input->isGivenKind(T_DOC_COMMENT)) {
throw new \InvalidArgumentException('Input must be a T_DOC_COMMENT token.');
}
$tokens = [];
$content = $input->getContent();
$ignoredTextPosition = 0;
$currentPosition = 0;
$token = null;
while (false !== $nextAtPosition = strpos($content, '@', $currentPosition)) {
if (0 !== $nextAtPosition && !Preg::match('/\s/', $content[$nextAtPosition - 1])) {
$currentPosition = $nextAtPosition + 1;
continue;
}
$lexer = new DocLexer();
$lexer->setInput(substr($content, $nextAtPosition));
$scannedTokens = [];
$index = 0;
$nbScannedTokensToUse = 0;
$nbScopes = 0;
while (null !== $token = $lexer->peek()) {
if (0 === $index && DocLexer::T_AT !== $token['type']) {
break;
}
if (1 === $index) {
if (DocLexer::T_IDENTIFIER !== $token['type'] || \in_array($token['value'], $ignoredTags, true)) {
break;
}
$nbScannedTokensToUse = 2;
}
if ($index >= 2 && 0 === $nbScopes && !\in_array($token['type'], [DocLexer::T_NONE, DocLexer::T_OPEN_PARENTHESIS], true)) {
break;
}
$scannedTokens[] = $token;
if (DocLexer::T_OPEN_PARENTHESIS === $token['type']) {
++$nbScopes;
} elseif (DocLexer::T_CLOSE_PARENTHESIS === $token['type']) {
if (0 === --$nbScopes) {
$nbScannedTokensToUse = \count($scannedTokens);
break;
}
}
++$index;
}
if (0 !== $nbScopes) {
break;
}
if (0 !== $nbScannedTokensToUse) {
$ignoredTextLength = $nextAtPosition - $ignoredTextPosition;
if (0 !== $ignoredTextLength) {
$tokens[] = new Token(DocLexer::T_NONE, substr($content, $ignoredTextPosition, $ignoredTextLength));
}
$lastTokenEndIndex = 0;
foreach (\array_slice($scannedTokens, 0, $nbScannedTokensToUse) as $token) {
if (DocLexer::T_STRING === $token['type']) {
$token['value'] = '"'.str_replace('"', '""', $token['value']).'"';
}
$missingTextLength = $token['position'] - $lastTokenEndIndex;
if ($missingTextLength > 0) {
$tokens[] = new Token(DocLexer::T_NONE, substr(
$content,
$nextAtPosition + $lastTokenEndIndex,
$missingTextLength
));
}
$tokens[] = new Token($token['type'], $token['value']);
$lastTokenEndIndex = $token['position'] + \strlen($token['value']);
}
$currentPosition = $ignoredTextPosition = $nextAtPosition + $token['position'] + \strlen($token['value']);
} else {
$currentPosition = $nextAtPosition + 1;
}
}
if ($ignoredTextPosition < \strlen($content)) {
$tokens[] = new Token(DocLexer::T_NONE, substr($content, $ignoredTextPosition));
}
return self::fromArray($tokens);
}
/**
* Create token collection from array.
*
* @param Token[] $array the array to import
* @param ?bool $saveIndices save the numeric indices used in the original array, default is yes
*/
public static function fromArray($array, $saveIndices = null): self
{
$tokens = new self(\count($array));
if (null === $saveIndices || $saveIndices) {
foreach ($array as $key => $val) {
$tokens[$key] = $val;
}
} else {
$index = 0;
foreach ($array as $val) {
$tokens[$index++] = $val;
}
}
return $tokens;
}
/**
* Returns the index of the closest next token that is neither a comment nor a whitespace token.
*/
public function getNextMeaningfulToken(int $index): ?int
{
return $this->getMeaningfulTokenSibling($index, 1);
}
/**
* Returns the index of the closest previous token that is neither a comment nor a whitespace token.
*/
public function getPreviousMeaningfulToken(int $index): ?int
{
return $this->getMeaningfulTokenSibling($index, -1);
}
/**
* Returns the index of the last token that is part of the annotation at the given index.
*/
public function getAnnotationEnd(int $index): ?int
{
$currentIndex = null;
if (isset($this[$index + 2])) {
if ($this[$index + 2]->isType(DocLexer::T_OPEN_PARENTHESIS)) {
$currentIndex = $index + 2;
} elseif (
isset($this[$index + 3])
&& $this[$index + 2]->isType(DocLexer::T_NONE)
&& $this[$index + 3]->isType(DocLexer::T_OPEN_PARENTHESIS)
&& Preg::match('/^(\R\s*\*\s*)*\s*$/', $this[$index + 2]->getContent())
) {
$currentIndex = $index + 3;
}
}
if (null !== $currentIndex) {
$level = 0;
for ($max = \count($this); $currentIndex < $max; ++$currentIndex) {
if ($this[$currentIndex]->isType(DocLexer::T_OPEN_PARENTHESIS)) {
++$level;
} elseif ($this[$currentIndex]->isType(DocLexer::T_CLOSE_PARENTHESIS)) {
--$level;
}
if (0 === $level) {
return $currentIndex;
}
}
return null;
}
return $index + 1;
}
/**
* Returns the code from the tokens.
*/
public function getCode(): string
{
$code = '';
foreach ($this as $token) {
$code .= $token->getContent();
}
return $code;
}
/**
* Inserts a token at the given index.
*/
public function insertAt(int $index, Token $token): void
{
$this->setSize($this->getSize() + 1);
for ($i = $this->getSize() - 1; $i > $index; --$i) {
$this[$i] = $this[$i - 1] ?? new Token();
}
$this[$index] = $token;
}
public function offsetSet($index, $token): void
{
// @phpstan-ignore-next-line as we type checking here
if (null === $token) {
throw new \InvalidArgumentException('Token must be an instance of PhpCsFixer\\Doctrine\\Annotation\\Token, "null" given.');
}
if (!$token instanceof Token) {
$type = \gettype($token);
if ('object' === $type) {
$type = \get_class($token);
}
throw new \InvalidArgumentException(sprintf('Token must be an instance of PhpCsFixer\\Doctrine\\Annotation\\Token, "%s" given.', $type));
}
parent::offsetSet($index, $token);
}
/**
* {@inheritdoc}
*
* @throws \OutOfBoundsException
*/
public function offsetUnset($index): void
{
if (!isset($this[$index])) {
throw new \OutOfBoundsException(sprintf('Index "%s" is invalid or does not exist.', $index));
}
$max = \count($this) - 1;
while ($index < $max) {
// @phpstan-ignore-next-line Next index always exists.
$this[$index] = $this[$index + 1];
++$index;
}
parent::offsetUnset($index);
$this->setSize($max);
}
private function getMeaningfulTokenSibling(int $index, int $direction): ?int
{
while (true) {
$index += $direction;
if (!$this->offsetExists($index)) {
break;
}
if (!$this[$index]->isType(DocLexer::T_NONE)) {
return $index;
}
}
return null;
}
}
@@ -0,0 +1,82 @@
<?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\Documentation;
use PhpCsFixer\Fixer\FixerInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\Utils;
/**
* @internal
*/
final class DocumentationLocator
{
private string $path;
public function __construct()
{
$this->path = \dirname(__DIR__, 2).'/doc';
}
public function getFixersDocumentationDirectoryPath(): string
{
return $this->path.'/rules';
}
public function getFixersDocumentationIndexFilePath(): string
{
return $this->getFixersDocumentationDirectoryPath().'/index.rst';
}
public function getFixerDocumentationFilePath(FixerInterface $fixer): string
{
return $this->getFixersDocumentationDirectoryPath().'/'.Preg::replaceCallback(
'/^.*\\\\(.+)\\\\(.+)Fixer$/',
static function (array $matches): string {
return Utils::camelCaseToUnderscore($matches[1]).'/'.Utils::camelCaseToUnderscore($matches[2]);
},
\get_class($fixer)
).'.rst';
}
public function getFixerDocumentationFileRelativePath(FixerInterface $fixer): string
{
return Preg::replace(
'#^'.preg_quote($this->getFixersDocumentationDirectoryPath(), '#').'/#',
'',
$this->getFixerDocumentationFilePath($fixer)
);
}
public function getRuleSetsDocumentationDirectoryPath(): string
{
return $this->path.'/ruleSets';
}
public function getRuleSetsDocumentationIndexFilePath(): string
{
return $this->getRuleSetsDocumentationDirectoryPath().'/index.rst';
}
public function getRuleSetsDocumentationFilePath(string $name): string
{
return $this->getRuleSetsDocumentationDirectoryPath().'/'.str_replace(':risky', 'Risky', ucfirst(substr($name, 1))).'.rst';
}
public function getListingFilePath(): string
{
return $this->path.'/list.rst';
}
}
@@ -0,0 +1,368 @@
<?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\Documentation;
use PhpCsFixer\Console\Command\HelpCommand;
use PhpCsFixer\Differ\FullDiffer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\DeprecatedFixerInterface;
use PhpCsFixer\Fixer\FixerInterface;
use PhpCsFixer\FixerConfiguration\AliasedFixerOption;
use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
use PhpCsFixer\FixerConfiguration\DeprecatedFixerOptionInterface;
use PhpCsFixer\FixerDefinition\CodeSampleInterface;
use PhpCsFixer\FixerDefinition\FileSpecificCodeSampleInterface;
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSampleInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\RuleSet\RuleSet;
use PhpCsFixer\RuleSet\RuleSets;
use PhpCsFixer\StdinFileInfo;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Utils;
/**
* @internal
*/
final class FixerDocumentGenerator
{
private DocumentationLocator $locator;
private FullDiffer $differ;
public function __construct(DocumentationLocator $locator)
{
$this->locator = $locator;
$this->differ = new FullDiffer();
}
public function generateFixerDocumentation(FixerInterface $fixer): string
{
$name = $fixer->getName();
$title = "Rule ``{$name}``";
$titleLine = str_repeat('=', \strlen($title));
$doc = "{$titleLine}\n{$title}\n{$titleLine}";
$definition = $fixer->getDefinition();
$doc .= "\n\n".RstUtils::toRst($definition->getSummary());
$description = $definition->getDescription();
if (null !== $description) {
$description = RstUtils::toRst($description);
$doc .= <<<RST
Description
-----------
{$description}
RST;
}
$deprecationDescription = '';
if ($fixer instanceof DeprecatedFixerInterface) {
$deprecationDescription = <<<'RST'
This rule is deprecated and will be removed on next major version
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
RST;
$alternatives = $fixer->getSuccessorsNames();
if (0 !== \count($alternatives)) {
$deprecationDescription .= RstUtils::toRst(sprintf(
"\n\nYou should use %s instead.",
Utils::naturalLanguageJoinWithBackticks($alternatives)
), 0);
}
}
$riskyDescription = '';
$riskyDescriptionRaw = $definition->getRiskyDescription();
if (null !== $riskyDescriptionRaw) {
$riskyDescriptionRaw = RstUtils::toRst($riskyDescriptionRaw, 0);
$riskyDescription = <<<RST
Using this rule is risky
~~~~~~~~~~~~~~~~~~~~~~~~
{$riskyDescriptionRaw}
RST;
}
if ($deprecationDescription || $riskyDescription) {
$warningsHeader = 'Warning';
if ($deprecationDescription && $riskyDescription) {
$warningsHeader = 'Warnings';
}
$warningsHeaderLine = str_repeat('-', \strlen($warningsHeader));
$doc .= "\n\n".implode("\n", array_filter([
$warningsHeader,
$warningsHeaderLine,
$deprecationDescription,
$riskyDescription,
]));
}
if ($fixer instanceof ConfigurableFixerInterface) {
$doc .= <<<'RST'
Configuration
-------------
RST;
$configurationDefinition = $fixer->getConfigurationDefinition();
foreach ($configurationDefinition->getOptions() as $option) {
$optionInfo = "``{$option->getName()}``";
$optionInfo .= "\n".str_repeat('~', \strlen($optionInfo));
if ($option instanceof DeprecatedFixerOptionInterface) {
$deprecationMessage = RstUtils::toRst($option->getDeprecationMessage());
$optionInfo .= "\n\n.. warning:: This option is deprecated and will be removed on next major version. {$deprecationMessage}";
}
$optionInfo .= "\n\n".RstUtils::toRst($option->getDescription());
if ($option instanceof AliasedFixerOption) {
$optionInfo .= "\n\n.. note:: The previous name of this option was ``{$option->getAlias()}`` but it is now deprecated and will be removed on next major version.";
}
$allowed = HelpCommand::getDisplayableAllowedValues($option);
if (null === $allowed) {
$allowedKind = 'Allowed types';
$allowed = array_map(
static fn ($value): string => '``'.$value.'``',
$option->getAllowedTypes(),
);
} else {
$allowedKind = 'Allowed values';
$allowed = array_map(static function ($value): string {
return $value instanceof AllowedValueSubset
? 'a subset of ``'.HelpCommand::toString($value->getAllowedValues()).'``'
: '``'.HelpCommand::toString($value).'``';
}, $allowed);
}
$allowed = implode(', ', $allowed);
$optionInfo .= "\n\n{$allowedKind}: {$allowed}";
if ($option->hasDefault()) {
$default = HelpCommand::toString($option->getDefault());
$optionInfo .= "\n\nDefault value: ``{$default}``";
} else {
$optionInfo .= "\n\nThis option is required.";
}
$doc .= "\n\n{$optionInfo}";
}
}
$samples = $definition->getCodeSamples();
if (0 !== \count($samples)) {
$doc .= <<<'RST'
Examples
--------
RST;
foreach ($samples as $index => $sample) {
$title = sprintf('Example #%d', $index + 1);
$titleLine = str_repeat('~', \strlen($title));
$doc .= "\n\n{$title}\n{$titleLine}";
if ($fixer instanceof ConfigurableFixerInterface) {
if (null === $sample->getConfiguration()) {
$doc .= "\n\n*Default* configuration.";
} else {
$doc .= sprintf(
"\n\nWith configuration: ``%s``.",
HelpCommand::toString($sample->getConfiguration())
);
}
}
$doc .= "\n".$this->generateSampleDiff($fixer, $sample, $index + 1, $name);
}
}
$ruleSetConfigs = [];
foreach (RuleSets::getSetDefinitionNames() as $set) {
$ruleSet = new RuleSet([$set => true]);
if ($ruleSet->hasRule($name)) {
$ruleSetConfigs[$set] = $ruleSet->getRuleConfiguration($name);
}
}
if ([] !== $ruleSetConfigs) {
$plural = 1 !== \count($ruleSetConfigs) ? 's' : '';
$doc .= <<<RST
Rule sets
---------
The rule is part of the following rule set{$plural}:
RST;
foreach ($ruleSetConfigs as $set => $config) {
$ruleSetPath = $this->locator->getRuleSetsDocumentationFilePath($set);
$ruleSetPath = substr($ruleSetPath, strrpos($ruleSetPath, '/'));
$doc .= <<<RST
{$set}
Using the `{$set} <./../../ruleSets{$ruleSetPath}>`_ rule set will enable the ``{$name}`` rule
RST;
if (null !== $config) {
$doc .= " with the config below:\n\n ``".HelpCommand::toString($config).'``';
} elseif ($fixer instanceof ConfigurableFixerInterface) {
$doc .= ' with the default config.';
} else {
$doc .= '.';
}
}
}
return "{$doc}\n";
}
/**
* @param FixerInterface[] $fixers
*/
public function generateFixersDocumentationIndex(array $fixers): string
{
$overrideGroups = [
'PhpUnit' => 'PHPUnit',
'PhpTag' => 'PHP Tag',
'Phpdoc' => 'PHPDoc',
];
usort($fixers, static function (FixerInterface $a, FixerInterface $b): int {
return strcmp(\get_class($a), \get_class($b));
});
$documentation = <<<'RST'
=======================
List of Available Rules
=======================
RST;
$currentGroup = null;
foreach ($fixers as $fixer) {
$namespace = Preg::replace('/^.*\\\\(.+)\\\\.+Fixer$/', '$1', \get_class($fixer));
$group = $overrideGroups[$namespace] ?? Preg::replace('/(?<=[[:lower:]])(?=[[:upper:]])/', ' ', $namespace);
if ($group !== $currentGroup) {
$underline = str_repeat('-', \strlen($group));
$documentation .= "\n\n{$group}\n{$underline}\n";
$currentGroup = $group;
}
$path = './'.$this->locator->getFixerDocumentationFileRelativePath($fixer);
$attributes = [];
if ($fixer instanceof DeprecatedFixerInterface) {
$attributes[] = 'deprecated';
}
if ($fixer->isRisky()) {
$attributes[] = 'risky';
}
$attributes = 0 === \count($attributes)
? ''
: ' *('.implode(', ', $attributes).')*'
;
$summary = str_replace('`', '``', $fixer->getDefinition()->getSummary());
$documentation .= <<<RST
- `{$fixer->getName()} <{$path}>`_{$attributes}
{$summary}
RST;
}
return "{$documentation}\n";
}
private function generateSampleDiff(FixerInterface $fixer, CodeSampleInterface $sample, int $sampleNumber, string $ruleName): string
{
if ($sample instanceof VersionSpecificCodeSampleInterface && !$sample->isSuitableFor(\PHP_VERSION_ID)) {
$existingFile = @file_get_contents($this->locator->getFixerDocumentationFilePath($fixer));
if (false !== $existingFile) {
Preg::match("/\\RExample #{$sampleNumber}\\R.+?(?<diff>\\R\\.\\. code-block:: diff\\R\\R.*?)\\R(?:\\R\\S|$)/s", $existingFile, $matches);
if (isset($matches['diff'])) {
return $matches['diff'];
}
}
$error = <<<RST
.. error::
Cannot generate diff for code sample #{$sampleNumber} of rule {$ruleName}:
the sample is not suitable for current version of PHP (%s).
RST;
return sprintf($error, PHP_VERSION);
}
$old = $sample->getCode();
$tokens = Tokens::fromCode($old);
$file = $sample instanceof FileSpecificCodeSampleInterface
? $sample->getSplFileInfo()
: new StdinFileInfo()
;
if ($fixer instanceof ConfigurableFixerInterface) {
$fixer->configure($sample->getConfiguration() ?? []);
}
$fixer->fix($file, $tokens);
$diff = $this->differ->diff($old, $tokens->generateCode());
$diff = Preg::replace('/@@[ \+\-\d,]+@@\n/', '', $diff);
$diff = Preg::replace('/\r/', '^M', $diff);
$diff = Preg::replace('/^ $/m', '', $diff);
$diff = Preg::replace('/\n$/', '', $diff);
$diff = RstUtils::indent($diff, 3);
return <<<RST
.. code-block:: diff
{$diff}
RST;
}
}
@@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Documentation;
use PhpCsFixer\Console\Command\HelpCommand;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\DeprecatedFixerInterface;
use PhpCsFixer\Fixer\FixerInterface;
use PhpCsFixer\FixerConfiguration\AliasedFixerOption;
use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
use PhpCsFixer\FixerConfiguration\DeprecatedFixerOptionInterface;
use PhpCsFixer\RuleSet\RuleSet;
use PhpCsFixer\RuleSet\RuleSets;
use PhpCsFixer\Utils;
/**
* @internal
*/
final class ListDocumentGenerator
{
private DocumentationLocator $locator;
public function __construct(DocumentationLocator $locator)
{
$this->locator = $locator;
}
/**
* @param FixerInterface[] $fixers
*/
public function generateListingDocumentation(array $fixers): string
{
usort(
$fixers,
static function (FixerInterface $fixer1, FixerInterface $fixer2): int {
return strnatcasecmp($fixer1->getName(), $fixer2->getName());
}
);
$documentation = <<<'RST'
=======================
List of Available Rules
=======================
RST;
foreach ($fixers as $fixer) {
$name = $fixer->getName();
$definition = $fixer->getDefinition();
$path = './rules/'.$this->locator->getFixerDocumentationFileRelativePath($fixer);
$documentation .= "\n- `{$name} <{$path}>`_\n";
$documentation .= "\n ".str_replace('`', '``', $definition->getSummary())."\n";
$description = $definition->getDescription();
if (null !== $description) {
$documentation .= "\n ".RstUtils::toRst($description, 3)."\n";
}
if ($fixer instanceof DeprecatedFixerInterface) {
$documentation .= "\n *warning deprecated*";
$alternatives = $fixer->getSuccessorsNames();
if (0 !== \count($alternatives)) {
$documentation .= RstUtils::toRst(sprintf(
' Use %s instead.',
Utils::naturalLanguageJoinWithBackticks($alternatives)
), 3);
}
$documentation .= "\n";
}
if ($fixer->isRisky()) {
$documentation .= "\n *warning risky* ".RstUtils::toRst($definition->getRiskyDescription(), 3)."\n";
}
if ($fixer instanceof ConfigurableFixerInterface) {
$documentation .= "\n Configuration options:\n";
$configurationDefinition = $fixer->getConfigurationDefinition();
foreach ($configurationDefinition->getOptions() as $option) {
$documentation .= "\n - | ``{$option->getName()}``";
$documentation .= "\n | {$option->getDescription()}";
if ($option instanceof DeprecatedFixerOptionInterface) {
$deprecationMessage = RstUtils::toRst($option->getDeprecationMessage(), 3);
$documentation .= "\n | warning:: This option is deprecated and will be removed on next major version. {$deprecationMessage}";
}
if ($option instanceof AliasedFixerOption) {
$documentation .= "\n | note:: The previous name of this option was ``{$option->getAlias()}`` but it is now deprecated and will be removed on next major version.";
}
$allowed = HelpCommand::getDisplayableAllowedValues($option);
if (null === $allowed) {
$allowedKind = 'Allowed types';
$allowed = array_map(
static fn ($value): string => '``'.$value.'``',
$option->getAllowedTypes(),
);
} else {
$allowedKind = 'Allowed values';
$allowed = array_map(static function ($value): string {
return $value instanceof AllowedValueSubset
? 'a subset of ``'.HelpCommand::toString($value->getAllowedValues()).'``'
: '``'.HelpCommand::toString($value).'``';
}, $allowed);
}
$allowed = implode(', ', $allowed);
$documentation .= "\n | {$allowedKind}: {$allowed}";
if ($option->hasDefault()) {
$default = HelpCommand::toString($option->getDefault());
$documentation .= "\n | Default value: ``{$default}``";
} else {
$documentation .= "\n | This option is required.";
}
}
$documentation .= "\n\n";
}
$ruleSetConfigs = [];
foreach (RuleSets::getSetDefinitionNames() as $set) {
$ruleSet = new RuleSet([$set => true]);
if ($ruleSet->hasRule($name)) {
$ruleSetConfigs[$set] = $ruleSet->getRuleConfiguration($name);
}
}
if ([] !== $ruleSetConfigs) {
$plural = 1 !== \count($ruleSetConfigs) ? 's' : '';
$documentation .= "\n Part of rule set{$plural} ";
foreach ($ruleSetConfigs as $set => $config) {
$ruleSetPath = $this->locator->getRuleSetsDocumentationFilePath($set);
$ruleSetPath = substr($ruleSetPath, strrpos($ruleSetPath, '/'));
$documentation .= "`{$set} <./ruleSets{$ruleSetPath}>`_ ";
}
$documentation = rtrim($documentation)."\n";
}
$reflectionObject = new \ReflectionObject($fixer);
$className = str_replace('\\', '\\\\', $reflectionObject->getName());
$fileName = $reflectionObject->getFileName();
$fileName = str_replace('\\', '/', $fileName);
$fileName = substr($fileName, strrpos($fileName, '/src/Fixer/') + 1);
$fileName = "`Source {$className} <./../{$fileName}>`_";
$documentation .= "\n ".$fileName;
}
return $documentation."\n";
}
}
@@ -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\Documentation;
use PhpCsFixer\Preg;
/**
* @internal
*/
final class RstUtils
{
private function __construct()
{
// cannot create instance of util. class
}
public static function toRst(string $string, int $indent = 0): string
{
$string = wordwrap(Preg::replace('/(?<!`)(`.*?`)(?!`)/', '`$1`', $string), 80 - $indent);
return 0 === $indent ? $string : self::indent($string, $indent);
}
public static function indent(string $string, int $indent): string
{
return Preg::replace('/(\n)(?!\n|$)/', '$1'.str_repeat(' ', $indent), $string);
}
}
@@ -0,0 +1,104 @@
<?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\Documentation;
use PhpCsFixer\Console\Command\HelpCommand;
use PhpCsFixer\Fixer\FixerInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\RuleSet\RuleSetDescriptionInterface;
/**
* @internal
*/
final class RuleSetDocumentationGenerator
{
private DocumentationLocator $locator;
public function __construct(DocumentationLocator $locator)
{
$this->locator = $locator;
}
/**
* @param FixerInterface[] $fixers
*/
public function generateRuleSetsDocumentation(RuleSetDescriptionInterface $definition, array $fixers): string
{
$fixerNames = [];
foreach ($fixers as $fixer) {
$fixerNames[$fixer->getName()] = $fixer;
}
$title = "Rule set ``{$definition->getName()}``";
$titleLine = str_repeat('=', \strlen($title));
$doc = "{$titleLine}\n{$title}\n{$titleLine}\n\n".$definition->getDescription();
if ($definition->isRisky()) {
$doc .= ' This set contains rules that are risky.';
}
$doc .= "\n\n";
$rules = $definition->getRules();
if (\count($rules) < 1) {
$doc .= 'This is an empty set.';
} else {
$doc .= "Rules\n-----\n";
foreach ($rules as $rule => $config) {
if (str_starts_with($rule, '@')) {
$ruleSetPath = $this->locator->getRuleSetsDocumentationFilePath($rule);
$ruleSetPath = substr($ruleSetPath, strrpos($ruleSetPath, '/'));
$doc .= "\n- `{$rule} <.{$ruleSetPath}>`_";
} else {
$path = Preg::replace(
'#^'.preg_quote($this->locator->getFixersDocumentationDirectoryPath(), '#').'/#',
'./../rules/',
$this->locator->getFixerDocumentationFilePath($fixerNames[$rule])
);
$doc .= "\n- `{$rule} <{$path}>`_";
}
if (!\is_bool($config)) {
$doc .= "\n config:\n ``".HelpCommand::toString($config).'``';
}
}
}
return $doc."\n";
}
/**
* @param array<string, string> $setDefinitions
*/
public function generateRuleSetsDocumentationIndex(array $setDefinitions): string
{
$documentation = <<<'RST'
===========================
List of Available Rule sets
===========================
RST;
foreach ($setDefinitions as $name => $path) {
$path = substr($path, strrpos($path, '/'));
$documentation .= "\n- `{$name} <.{$path}>`_";
}
return $documentation."\n";
}
}
@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Error;
/**
* An abstraction for errors that can occur before and during fixing.
*
* @author Andreas Möller <am@localheinz.com>
*
* @internal
*/
final class Error
{
/**
* Error which has occurred in linting phase, before applying any fixers.
*/
public const TYPE_INVALID = 1;
/**
* Error which has occurred during fixing phase.
*/
public const TYPE_EXCEPTION = 2;
/**
* Error which has occurred in linting phase, after applying any fixers.
*/
public const TYPE_LINT = 3;
private int $type;
private string $filePath;
private ?\Throwable $source;
/**
* @var list<string>
*/
private array $appliedFixers;
private ?string $diff;
/**
* @param list<string> $appliedFixers
*/
public function __construct(int $type, string $filePath, ?\Throwable $source = null, array $appliedFixers = [], ?string $diff = null)
{
$this->type = $type;
$this->filePath = $filePath;
$this->source = $source;
$this->appliedFixers = $appliedFixers;
$this->diff = $diff;
}
public function getFilePath(): string
{
return $this->filePath;
}
public function getSource(): ?\Throwable
{
return $this->source;
}
public function getType(): int
{
return $this->type;
}
/**
* @return list<string>
*/
public function getAppliedFixers(): array
{
return $this->appliedFixers;
}
public function getDiff(): ?string
{
return $this->diff;
}
}
@@ -0,0 +1,79 @@
<?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\Error;
/**
* Manager of errors that occur during fixing.
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class ErrorsManager
{
/**
* @var Error[]
*/
private array $errors = [];
/**
* Returns errors reported during linting before fixing.
*
* @return Error[]
*/
public function getInvalidErrors(): array
{
return array_filter($this->errors, static function (Error $error): bool {
return Error::TYPE_INVALID === $error->getType();
});
}
/**
* Returns errors reported during fixing.
*
* @return Error[]
*/
public function getExceptionErrors(): array
{
return array_filter($this->errors, static function (Error $error): bool {
return Error::TYPE_EXCEPTION === $error->getType();
});
}
/**
* Returns errors reported during linting after fixing.
*
* @return Error[]
*/
public function getLintErrors(): array
{
return array_filter($this->errors, static function (Error $error): bool {
return Error::TYPE_LINT === $error->getType();
});
}
/**
* Returns true if no errors were reported.
*/
public function isEmpty(): bool
{
return empty($this->errors);
}
public function report(Error $error): void
{
$this->errors[] = $error;
}
}
@@ -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;
/**
* File reader that unify access to regular file and stdin-alike file.
*
* Regular file could be read multiple times with `file_get_contents`, but file provided on stdin cannot.
* Consecutive try will provide empty content for stdin-alike file.
* This reader unifies access to them.
*
* @internal
*/
final class FileReader
{
/**
* @var null|string
*/
private $stdinContent;
public static function createSingleton(): self
{
static $instance = null;
if (!$instance) {
$instance = new self();
}
return $instance;
}
public function read(string $filePath): string
{
if ('php://stdin' === $filePath) {
if (null === $this->stdinContent) {
$this->stdinContent = $this->readRaw($filePath);
}
return $this->stdinContent;
}
return $this->readRaw($filePath);
}
private function readRaw(string $realPath): string
{
$content = @file_get_contents($realPath);
if (false === $content) {
$error = error_get_last();
throw new \RuntimeException(sprintf(
'Failed to read content from "%s".%s',
$realPath,
$error ? ' '.$error['message'] : ''
));
}
return $content;
}
}
@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer;
/**
* Handles files removal with possibility to remove them on shutdown.
*
* @author Adam Klvač <adam@klva.cz>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* @internal
*/
final class FileRemoval
{
/**
* List of observed files to be removed.
*
* @var array<string, true>
*/
private array $files = [];
public function __construct()
{
register_shutdown_function([$this, 'clean']);
}
public function __destruct()
{
$this->clean();
}
/**
* This class is not intended to be serialized,
* and cannot be deserialized (see __wakeup method).
*/
public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
/**
* Disable the deserialization of the class to prevent attacker executing
* code by leveraging the __destruct method.
*
* @see https://owasp.org/www-community/vulnerabilities/PHP_Object_Injection
*/
public function __wakeup(): void
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
/**
* Adds a file to be removed.
*/
public function observe(string $path): void
{
$this->files[$path] = true;
}
/**
* Removes a file from shutdown removal.
*/
public function delete(string $path): void
{
if (isset($this->files[$path])) {
unset($this->files[$path]);
}
$this->unlink($path);
}
/**
* Removes attached files.
*/
public function clean(): void
{
foreach ($this->files as $file => $value) {
$this->unlink($file);
}
$this->files = [];
}
private function unlink(string $path): void
{
@unlink($path);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?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;
use Symfony\Component\Finder\Finder as BaseFinder;
/**
* @author Fabien Potencier <fabien@symfony.com>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
class Finder extends BaseFinder
{
public function __construct()
{
parent::__construct();
$this
->files()
->name('/\.php$/')
->exclude('vendor')
;
}
}
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\Tokenizer\Tokens;
abstract class AbstractIncrementOperatorFixer extends AbstractFixer
{
final protected function findStart(Tokens $tokens, int $index): int
{
do {
$index = $tokens->getPrevMeaningfulToken($index);
$token = $tokens[$index];
$blockType = Tokens::detectBlockType($token);
if (null !== $blockType && !$blockType['isStart']) {
$index = $tokens->findBlockStart($blockType['type'], $index);
$token = $tokens[$index];
}
} while (!$token->equalsAny(['$', [T_VARIABLE]]));
$prevIndex = $tokens->getPrevMeaningfulToken($index);
$prevToken = $tokens[$prevIndex];
if ($prevToken->equals('$')) {
return $this->findStart($tokens, $index);
}
if ($prevToken->isObjectOperator()) {
return $this->findStart($tokens, $prevIndex);
}
if ($prevToken->isGivenKind(T_PAAMAYIM_NEKUDOTAYIM)) {
$prevPrevIndex = $tokens->getPrevMeaningfulToken($prevIndex);
if (!$tokens[$prevPrevIndex]->isGivenKind([T_STATIC, T_STRING])) {
return $this->findStart($tokens, $prevIndex);
}
$index = $tokens->getTokenNotOfKindsSibling($prevIndex, -1, [T_NS_SEPARATOR, T_STATIC, T_STRING]);
$index = $tokens->getNextMeaningfulToken($index);
}
return $index;
}
}
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\Indicator\PhpUnitTestCaseIndicator;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @internal
*/
abstract class AbstractPhpUnitFixer extends AbstractFixer
{
/**
* {@inheritdoc}
*/
final public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAllTokenKindsFound([T_CLASS, T_STRING]);
}
final protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$phpUnitTestCaseIndicator = new PhpUnitTestCaseIndicator();
foreach ($phpUnitTestCaseIndicator->findPhpUnitClasses($tokens) as $indices) {
$this->applyPhpUnitClassFix($tokens, $indices[0], $indices[1]);
}
}
abstract protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void;
final protected function getDocBlockIndex(Tokens $tokens, int $index): int
{
do {
$index = $tokens->getPrevNonWhitespace($index);
} while ($tokens[$index]->isGivenKind([T_PUBLIC, T_PROTECTED, T_PRIVATE, T_FINAL, T_ABSTRACT, T_COMMENT]));
return $index;
}
final protected function isPHPDoc(Tokens $tokens, int $index): bool
{
return $tokens[$index]->isGivenKind(T_DOC_COMMENT);
}
}
@@ -0,0 +1,216 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\Alias;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
final class ArrayPushFixer extends AbstractFixer
{
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Converts simple usages of `array_push($x, $y);` to `$x[] = $y;`.',
[new CodeSample("<?php\narray_push(\$x, \$y);\n")],
null,
'Risky when the function `array_push` is overridden.'
);
}
/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(T_STRING) && $tokens->count() > 7;
}
/**
* {@inheritdoc}
*/
public function isRisky(): bool
{
return true;
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$functionsAnalyzer = new FunctionsAnalyzer();
for ($index = $tokens->count() - 7; $index > 0; --$index) {
if (!$tokens[$index]->equals([T_STRING, 'array_push'], false)) {
continue;
}
if (!$functionsAnalyzer->isGlobalFunctionCall($tokens, $index)) {
continue; // redeclare/override
}
// meaningful before must be `<?php`, `{`, `}` or `;`
$callIndex = $index;
$index = $tokens->getPrevMeaningfulToken($index);
$namespaceSeparatorIndex = null;
if ($tokens[$index]->isGivenKind(T_NS_SEPARATOR)) {
$namespaceSeparatorIndex = $index;
$index = $tokens->getPrevMeaningfulToken($index);
}
if (!$tokens[$index]->equalsAny([';', '{', '}', ')', [T_OPEN_TAG]])) {
continue;
}
// figure out where the arguments list opens
$openBraceIndex = $tokens->getNextMeaningfulToken($callIndex);
$blockType = Tokens::detectBlockType($tokens[$openBraceIndex]);
if (null === $blockType || Tokens::BLOCK_TYPE_PARENTHESIS_BRACE !== $blockType['type']) {
continue;
}
// figure out where the arguments list closes
$closeBraceIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openBraceIndex);
// meaningful after `)` must be `;`, `? >` or nothing
$afterCloseBraceIndex = $tokens->getNextMeaningfulToken($closeBraceIndex);
if (null !== $afterCloseBraceIndex && !$tokens[$afterCloseBraceIndex]->equalsAny([';', [T_CLOSE_TAG]])) {
continue;
}
// must have 2 arguments
// first argument must be a variable (with possibly array indexing etc.),
// after that nothing meaningful should be there till the next `,` or `)`
// if `)` than we cannot fix it (it is a single argument call)
$firstArgumentStop = $this->getFirstArgumentEnd($tokens, $openBraceIndex);
$firstArgumentStop = $tokens->getNextMeaningfulToken($firstArgumentStop);
if (!$tokens[$firstArgumentStop]->equals(',')) {
return;
}
// second argument can be about anything but ellipsis, we must make sure there is not
// a third argument (or more) passed to `array_push`
$secondArgumentStart = $tokens->getNextMeaningfulToken($firstArgumentStop);
$secondArgumentStop = $this->getSecondArgumentEnd($tokens, $secondArgumentStart, $closeBraceIndex);
if (null === $secondArgumentStop) {
continue;
}
// candidate is valid, replace tokens
$tokens->clearTokenAndMergeSurroundingWhitespace($closeBraceIndex);
$tokens->clearTokenAndMergeSurroundingWhitespace($firstArgumentStop);
$tokens->insertAt(
$firstArgumentStop,
[
new Token('['),
new Token(']'),
new Token([T_WHITESPACE, ' ']),
new Token('='),
]
);
$tokens->clearTokenAndMergeSurroundingWhitespace($openBraceIndex);
$tokens->clearTokenAndMergeSurroundingWhitespace($callIndex);
if (null !== $namespaceSeparatorIndex) {
$tokens->clearTokenAndMergeSurroundingWhitespace($namespaceSeparatorIndex);
}
}
}
private function getFirstArgumentEnd(Tokens $tokens, int $index): int
{
$nextIndex = $tokens->getNextMeaningfulToken($index);
$nextToken = $tokens[$nextIndex];
while ($nextToken->equalsAny([
'$',
'[',
'(',
[CT::T_ARRAY_INDEX_CURLY_BRACE_OPEN],
[CT::T_DYNAMIC_PROP_BRACE_OPEN],
[CT::T_DYNAMIC_VAR_BRACE_OPEN],
[CT::T_NAMESPACE_OPERATOR],
[T_NS_SEPARATOR],
[T_STATIC],
[T_STRING],
[T_VARIABLE],
])) {
$blockType = Tokens::detectBlockType($nextToken);
if (null !== $blockType) {
$nextIndex = $tokens->findBlockEnd($blockType['type'], $nextIndex);
}
$index = $nextIndex;
$nextIndex = $tokens->getNextMeaningfulToken($nextIndex);
$nextToken = $tokens[$nextIndex];
}
if ($nextToken->isGivenKind(T_OBJECT_OPERATOR)) {
return $this->getFirstArgumentEnd($tokens, $nextIndex);
}
if ($nextToken->isGivenKind(T_PAAMAYIM_NEKUDOTAYIM)) {
return $this->getFirstArgumentEnd($tokens, $tokens->getNextMeaningfulToken($nextIndex));
}
return $index;
}
/**
* @param int $endIndex boundary, i.e. tokens index of `)`
*/
private function getSecondArgumentEnd(Tokens $tokens, int $index, int $endIndex): ?int
{
if ($tokens[$index]->isGivenKind(T_ELLIPSIS)) {
return null;
}
for (; $index <= $endIndex; ++$index) {
$blockType = Tokens::detectBlockType($tokens[$index]);
while (null !== $blockType && $blockType['isStart']) {
$index = $tokens->findBlockEnd($blockType['type'], $index);
$index = $tokens->getNextMeaningfulToken($index);
$blockType = Tokens::detectBlockType($tokens[$index]);
}
if ($tokens[$index]->equals(',') || $tokens[$index]->isGivenKind([T_YIELD, T_YIELD_FROM, T_LOGICAL_AND, T_LOGICAL_OR, T_LOGICAL_XOR])) {
return null;
}
}
return $endIndex;
}
}
@@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\Alias;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Filippo Tessarotto <zoeslam@gmail.com>
*/
final class BacktickToShellExecFixer extends AbstractFixer
{
/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound('`');
}
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Converts backtick operators to `shell_exec` calls.',
[
new CodeSample(
<<<'EOT'
<?php
$plain = `ls -lah`;
$withVar = `ls -lah $var1 ${var2} {$var3} {$var4[0]} {$var5->call()}`;
EOT
),
],
'Conversion is done only when it is non risky, so when special chars like single-quotes, double-quotes and backticks are not used inside the command.'
);
}
/**
* {@inheritdoc}
*
* Must run before EscapeImplicitBackslashesFixer, ExplicitStringVariableFixer, NativeFunctionInvocationFixer, SingleQuoteFixer.
*/
public function getPriority(): int
{
return 17;
}
/**
* {@inheritdoc}
*/
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$backtickStarted = false;
$backtickTokens = [];
for ($index = $tokens->count() - 1; $index > 0; --$index) {
$token = $tokens[$index];
if (!$token->equals('`')) {
if ($backtickStarted) {
$backtickTokens[$index] = $token;
}
continue;
}
$backtickTokens[$index] = $token;
if ($backtickStarted) {
$this->fixBackticks($tokens, $backtickTokens);
$backtickTokens = [];
}
$backtickStarted = !$backtickStarted;
}
}
/**
* Override backtick code with corresponding double-quoted string.
*
* @param array<int, Token> $backtickTokens
*/
private function fixBackticks(Tokens $tokens, array $backtickTokens): void
{
// Track indices for final override
ksort($backtickTokens);
$openingBacktickIndex = key($backtickTokens);
end($backtickTokens);
$closingBacktickIndex = key($backtickTokens);
// Strip enclosing backticks
array_shift($backtickTokens);
array_pop($backtickTokens);
// Double-quoted strings are parsed differently if they contain
// variables or not, so we need to build the new token array accordingly
$count = \count($backtickTokens);
$newTokens = [
new Token([T_STRING, 'shell_exec']),
new Token('('),
];
if (1 !== $count) {
$newTokens[] = new Token('"');
}
foreach ($backtickTokens as $token) {
if (!$token->isGivenKind(T_ENCAPSED_AND_WHITESPACE)) {
$newTokens[] = $token;
continue;
}
$content = $token->getContent();
// Escaping special chars depends on the context: too tricky
if (Preg::match('/[`"\']/u', $content)) {
return;
}
$kind = T_ENCAPSED_AND_WHITESPACE;
if (1 === $count) {
$content = '"'.$content.'"';
$kind = T_CONSTANT_ENCAPSED_STRING;
}
$newTokens[] = new Token([$kind, $content]);
}
if (1 !== $count) {
$newTokens[] = new Token('"');
}
$newTokens[] = new Token(')');
$tokens->overrideRange($openingBacktickIndex, $closingBacktickIndex, $newTokens);
}
}
@@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\Alias;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Preg;
use PhpCsFixer\PregException;
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Matteo Beccati <matteo@beccati.com>
*/
final class EregToPregFixer extends AbstractFixer
{
/**
* @var list<array<int, string>> the list of the ext/ereg function names, their preg equivalent and the preg modifier(s), if any
* all condensed in an array of arrays
*/
private static array $functions = [
['ereg', 'preg_match', ''],
['eregi', 'preg_match', 'i'],
['ereg_replace', 'preg_replace', ''],
['eregi_replace', 'preg_replace', 'i'],
['split', 'preg_split', ''],
['spliti', 'preg_split', 'i'],
];
/**
* @var list<string> the list of preg delimiters, in order of preference
*/
private static array $delimiters = ['/', '#', '!'];
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Replace deprecated `ereg` regular expression functions with `preg`.',
[new CodeSample("<?php \$x = ereg('[A-Z]');\n")],
null,
'Risky if the `ereg` function is overridden.'
);
}
/**
* {@inheritdoc}
*
* Must run after NoUselessConcatOperatorFixer.
*/
public function getPriority(): int
{
return 0;
}
/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(T_STRING);
}
/**
* {@inheritdoc}
*/
public function isRisky(): bool
{
return true;
}
/**
* {@inheritdoc}
*/
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$end = $tokens->count() - 1;
$functionsAnalyzer = new FunctionsAnalyzer();
foreach (self::$functions as $map) {
// the sequence is the function name, followed by "(" and a quoted string
$seq = [[T_STRING, $map[0]], '(', [T_CONSTANT_ENCAPSED_STRING]];
$currIndex = 0;
while (true) {
$match = $tokens->findSequence($seq, $currIndex, $end, false);
// did we find a match?
if (null === $match) {
break;
}
// findSequence also returns the tokens, but we're only interested in the indices, i.e.:
// 0 => function name,
// 1 => bracket "("
// 2 => quoted string passed as 1st parameter
$match = array_keys($match);
// advance tokenizer cursor
$currIndex = $match[2];
if (!$functionsAnalyzer->isGlobalFunctionCall($tokens, $match[0])) {
continue;
}
// ensure the first parameter is just a string (e.g. has nothing appended)
$next = $tokens->getNextMeaningfulToken($match[2]);
if (null === $next || !$tokens[$next]->equalsAny([',', ')'])) {
continue;
}
// convert to PCRE
$regexTokenContent = $tokens[$match[2]]->getContent();
if ('b' === $regexTokenContent[0] || 'B' === $regexTokenContent[0]) {
$quote = $regexTokenContent[1];
$prefix = $regexTokenContent[0];
$string = substr($regexTokenContent, 2, -1);
} else {
$quote = $regexTokenContent[0];
$prefix = '';
$string = substr($regexTokenContent, 1, -1);
}
$delim = $this->getBestDelimiter($string);
$preg = $delim.addcslashes($string, $delim).$delim.'D'.$map[2];
// check if the preg is valid
if (!$this->checkPreg($preg)) {
continue;
}
// modify function and argument
$tokens[$match[0]] = new Token([T_STRING, $map[1]]);
$tokens[$match[2]] = new Token([T_CONSTANT_ENCAPSED_STRING, $prefix.$quote.$preg.$quote]);
}
}
}
/**
* Check the validity of a PCRE.
*
* @param string $pattern the regular expression
*/
private function checkPreg(string $pattern): bool
{
try {
Preg::match($pattern, '');
return true;
} catch (PregException $e) {
return false;
}
}
/**
* Get the delimiter that would require the least escaping in a regular expression.
*
* @param string $pattern the regular expression
*
* @return string the preg delimiter
*/
private function getBestDelimiter(string $pattern): string
{
// try to find something that's not used
$delimiters = [];
foreach (self::$delimiters as $k => $d) {
if (!str_contains($pattern, $d)) {
return $d;
}
$delimiters[$d] = [substr_count($pattern, $d), $k];
}
// return the least used delimiter, using the position in the list as a tiebreaker
uasort($delimiters, static function (array $a, array $b): int {
if ($a[0] === $b[0]) {
return $a[1] <=> $b[1];
}
return $a[0] <=> $b[0];
});
return key($delimiters);
}
}
@@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\Alias;
use PhpCsFixer\AbstractFunctionReferenceFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Analyzer\ArgumentsAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Filippo Tessarotto <zoeslam@gmail.com>
*/
final class MbStrFunctionsFixer extends AbstractFunctionReferenceFixer
{
/**
* list of the string-related function names and their mb_ equivalent.
*
* @var array<
* string,
* array{
* alternativeName: string,
* argumentCount: list<int>,
* },
* >
*/
private static array $functionsMap = [
'str_split' => ['alternativeName' => 'mb_str_split', 'argumentCount' => [1, 2, 3]],
'stripos' => ['alternativeName' => 'mb_stripos', 'argumentCount' => [2, 3]],
'stristr' => ['alternativeName' => 'mb_stristr', 'argumentCount' => [2, 3]],
'strlen' => ['alternativeName' => 'mb_strlen', 'argumentCount' => [1]],
'strpos' => ['alternativeName' => 'mb_strpos', 'argumentCount' => [2, 3]],
'strrchr' => ['alternativeName' => 'mb_strrchr', 'argumentCount' => [2]],
'strripos' => ['alternativeName' => 'mb_strripos', 'argumentCount' => [2, 3]],
'strrpos' => ['alternativeName' => 'mb_strrpos', 'argumentCount' => [2, 3]],
'strstr' => ['alternativeName' => 'mb_strstr', 'argumentCount' => [2, 3]],
'strtolower' => ['alternativeName' => 'mb_strtolower', 'argumentCount' => [1]],
'strtoupper' => ['alternativeName' => 'mb_strtoupper', 'argumentCount' => [1]],
'substr' => ['alternativeName' => 'mb_substr', 'argumentCount' => [2, 3]],
'substr_count' => ['alternativeName' => 'mb_substr_count', 'argumentCount' => [2, 3, 4]],
];
/**
* @var array<
* string,
* array{
* alternativeName: string,
* argumentCount: list<int>,
* },
* >
*/
private array $functions;
public function __construct()
{
parent::__construct();
$this->functions = array_filter(
self::$functionsMap,
static function (array $mapping): bool {
return (new \ReflectionFunction($mapping['alternativeName']))->isInternal();
}
);
}
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Replace non multibyte-safe functions with corresponding mb function.',
[
new CodeSample(
'<?php
$a = strlen($a);
$a = strpos($a, $b);
$a = strrpos($a, $b);
$a = substr($a, $b);
$a = strtolower($a);
$a = strtoupper($a);
$a = stripos($a, $b);
$a = strripos($a, $b);
$a = strstr($a, $b);
$a = stristr($a, $b);
$a = strrchr($a, $b);
$a = substr_count($a, $b);
'
),
],
null,
'Risky when any of the functions are overridden, or when relying on the string byte size rather than its length in characters.'
);
}
/**
* {@inheritdoc}
*/
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$argumentsAnalyzer = new ArgumentsAnalyzer();
foreach ($this->functions as $functionIdentity => $functionReplacement) {
$currIndex = 0;
do {
// try getting function reference and translate boundaries for humans
$boundaries = $this->find($functionIdentity, $tokens, $currIndex, $tokens->count() - 1);
if (null === $boundaries) {
// next function search, as current one not found
continue 2;
}
[$functionName, $openParenthesis, $closeParenthesis] = $boundaries;
$count = $argumentsAnalyzer->countArguments($tokens, $openParenthesis, $closeParenthesis);
if (!\in_array($count, $functionReplacement['argumentCount'], true)) {
continue 2;
}
// analysing cursor shift, so nested calls could be processed
$currIndex = $openParenthesis;
$tokens[$functionName] = new Token([T_STRING, $functionReplacement['alternativeName']]);
} while (null !== $currIndex);
}
}
}
@@ -0,0 +1,233 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\Alias;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Analyzer\ArgumentsAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Alexander M. Turek <me@derrabus.de>
*/
final class ModernizeStrposFixer extends AbstractFixer
{
private const REPLACEMENTS = [
[
'operator' => [T_IS_IDENTICAL, '==='],
'operand' => [T_LNUMBER, '0'],
'replacement' => [T_STRING, 'str_starts_with'],
'negate' => false,
],
[
'operator' => [T_IS_NOT_IDENTICAL, '!=='],
'operand' => [T_LNUMBER, '0'],
'replacement' => [T_STRING, 'str_starts_with'],
'negate' => true,
],
[
'operator' => [T_IS_NOT_IDENTICAL, '!=='],
'operand' => [T_STRING, 'false'],
'replacement' => [T_STRING, 'str_contains'],
'negate' => false,
],
[
'operator' => [T_IS_IDENTICAL, '==='],
'operand' => [T_STRING, 'false'],
'replacement' => [T_STRING, 'str_contains'],
'negate' => true,
],
];
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Replace `strpos()` calls with `str_starts_with()` or `str_contains()` if possible.',
[
new CodeSample(
'<?php
if (strpos($haystack, $needle) === 0) {}
if (strpos($haystack, $needle) !== 0) {}
if (strpos($haystack, $needle) !== false) {}
if (strpos($haystack, $needle) === false) {}
'
),
],
null,
'Risky if `strpos`, `str_starts_with` or `str_contains` functions are overridden.'
);
}
/**
* {@inheritdoc}
*
* Must run before BinaryOperatorSpacesFixer, NoExtraBlankLinesFixer, NoSpacesInsideParenthesisFixer, NoTrailingWhitespaceFixer, NotOperatorWithSpaceFixer, NotOperatorWithSuccessorSpaceFixer, PhpUnitDedicateAssertFixer, SingleSpaceAfterConstructFixer.
* Must run after StrictComparisonFixer.
*/
public function getPriority(): int
{
return 37;
}
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(T_STRING) && $tokens->isAnyTokenKindsFound([T_IS_IDENTICAL, T_IS_NOT_IDENTICAL]);
}
public function isRisky(): bool
{
return true;
}
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$functionsAnalyzer = new FunctionsAnalyzer();
$argumentsAnalyzer = new ArgumentsAnalyzer();
for ($index = \count($tokens) - 1; $index > 0; --$index) {
// find candidate function call
if (!$tokens[$index]->equals([T_STRING, 'strpos'], false) || !$functionsAnalyzer->isGlobalFunctionCall($tokens, $index)) {
continue;
}
// assert called with 2 arguments
$openIndex = $tokens->getNextMeaningfulToken($index);
$closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openIndex);
$arguments = $argumentsAnalyzer->getArguments($tokens, $openIndex, $closeIndex);
if (2 !== \count($arguments)) {
continue;
}
// check if part condition and fix if needed
$compareTokens = $this->getCompareTokens($tokens, $index, -1); // look behind
if (null === $compareTokens) {
$compareTokens = $this->getCompareTokens($tokens, $closeIndex, 1); // look ahead
}
if (null !== $compareTokens) {
$this->fixCall($tokens, $index, $compareTokens);
}
}
}
/**
* @param array{operator_index: int, operand_index: int} $operatorIndices
*/
private function fixCall(Tokens $tokens, int $functionIndex, array $operatorIndices): void
{
foreach (self::REPLACEMENTS as $replacement) {
if (!$tokens[$operatorIndices['operator_index']]->equals($replacement['operator'])) {
continue;
}
if (!$tokens[$operatorIndices['operand_index']]->equals($replacement['operand'], false)) {
continue;
}
$tokens->clearTokenAndMergeSurroundingWhitespace($operatorIndices['operator_index']);
$tokens->clearTokenAndMergeSurroundingWhitespace($operatorIndices['operand_index']);
$tokens->clearTokenAndMergeSurroundingWhitespace($functionIndex);
if ($replacement['negate']) {
$negateInsertIndex = $functionIndex;
$prevFunctionIndex = $tokens->getPrevMeaningfulToken($functionIndex);
if ($tokens[$prevFunctionIndex]->isGivenKind(T_NS_SEPARATOR)) {
$negateInsertIndex = $prevFunctionIndex;
}
$tokens->insertAt($negateInsertIndex, new Token('!'));
++$functionIndex;
}
$tokens->insertAt($functionIndex, new Token($replacement['replacement']));
break;
}
}
/**
* @param -1|1 $direction
*
* @return null|array{operator_index: int, operand_index: int}
*/
private function getCompareTokens(Tokens $tokens, int $offsetIndex, int $direction): ?array
{
$operatorIndex = $tokens->getMeaningfulTokenSibling($offsetIndex, $direction);
if (null !== $operatorIndex && $tokens[$operatorIndex]->isGivenKind(T_NS_SEPARATOR)) {
$operatorIndex = $tokens->getMeaningfulTokenSibling($operatorIndex, $direction);
}
if (null === $operatorIndex || !$tokens[$operatorIndex]->isGivenKind([T_IS_IDENTICAL, T_IS_NOT_IDENTICAL])) {
return null;
}
$operandIndex = $tokens->getMeaningfulTokenSibling($operatorIndex, $direction);
if (null === $operandIndex) {
return null;
}
$operand = $tokens[$operandIndex];
if (!$operand->equals([T_LNUMBER, '0']) && !$operand->equals([T_STRING, 'false'], false)) {
return null;
}
$precedenceTokenIndex = $tokens->getMeaningfulTokenSibling($operandIndex, $direction);
if (null !== $precedenceTokenIndex && $this->isOfHigherPrecedence($tokens[$precedenceTokenIndex])) {
return null;
}
return ['operator_index' => $operatorIndex, 'operand_index' => $operandIndex];
}
private function isOfHigherPrecedence(Token $token): bool
{
static $operatorsKinds = [
T_DEC, // --
T_INC, // ++
T_INSTANCEOF, // instanceof
T_IS_GREATER_OR_EQUAL, // >=
T_IS_SMALLER_OR_EQUAL, // <=
T_POW, // **
T_SL, // <<
T_SR, // >>
];
static $operatorsPerContent = [
'!',
'%',
'*',
'+',
'-',
'.',
'/',
'<',
'>',
'~',
];
return $token->isGivenKind($operatorsKinds) || $token->equalsAny($operatorsPerContent);
}
}
@@ -0,0 +1,335 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\Alias;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Analyzer\ArgumentsAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Vladimir Reznichenko <kalessil@gmail.com>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class NoAliasFunctionsFixer extends AbstractFixer implements ConfigurableFixerInterface
{
private const SETS = [
'@internal' => [
'diskfreespace' => 'disk_free_space',
'dns_check_record' => 'checkdnsrr',
'dns_get_mx' => 'getmxrr',
'session_commit' => 'session_write_close',
'stream_register_wrapper' => 'stream_wrapper_register',
'set_file_buffer' => 'stream_set_write_buffer',
'socket_set_blocking' => 'stream_set_blocking',
'socket_get_status' => 'stream_get_meta_data',
'socket_set_timeout' => 'stream_set_timeout',
'socket_getopt' => 'socket_get_option',
'socket_setopt' => 'socket_set_option',
'chop' => 'rtrim',
'close' => 'closedir',
'doubleval' => 'floatval',
'fputs' => 'fwrite',
'get_required_files' => 'get_included_files',
'ini_alter' => 'ini_set',
'is_double' => 'is_float',
'is_integer' => 'is_int',
'is_long' => 'is_int',
'is_real' => 'is_float',
'is_writeable' => 'is_writable',
'join' => 'implode',
'key_exists' => 'array_key_exists',
'magic_quotes_runtime' => 'set_magic_quotes_runtime',
'pos' => 'current',
'show_source' => 'highlight_file',
'sizeof' => 'count',
'strchr' => 'strstr',
'user_error' => 'trigger_error',
],
'@IMAP' => [
'imap_create' => 'imap_createmailbox',
'imap_fetchtext' => 'imap_body',
'imap_header' => 'imap_headerinfo',
'imap_listmailbox' => 'imap_list',
'imap_listsubscribed' => 'imap_lsub',
'imap_rename' => 'imap_renamemailbox',
'imap_scan' => 'imap_listscan',
'imap_scanmailbox' => 'imap_listscan',
],
'@ldap' => [
'ldap_close' => 'ldap_unbind',
'ldap_modify' => 'ldap_mod_replace',
],
'@mysqli' => [
'mysqli_execute' => 'mysqli_stmt_execute',
'mysqli_set_opt' => 'mysqli_options',
'mysqli_escape_string' => 'mysqli_real_escape_string',
],
'@pg' => [
'pg_exec' => 'pg_query',
],
'@oci' => [
'oci_free_cursor' => 'oci_free_statement',
],
'@odbc' => [
'odbc_do' => 'odbc_exec',
'odbc_field_precision' => 'odbc_field_len',
],
'@mbreg' => [
'mbereg' => 'mb_ereg',
'mbereg_match' => 'mb_ereg_match',
'mbereg_replace' => 'mb_ereg_replace',
'mbereg_search' => 'mb_ereg_search',
'mbereg_search_getpos' => 'mb_ereg_search_getpos',
'mbereg_search_getregs' => 'mb_ereg_search_getregs',
'mbereg_search_init' => 'mb_ereg_search_init',
'mbereg_search_pos' => 'mb_ereg_search_pos',
'mbereg_search_regs' => 'mb_ereg_search_regs',
'mbereg_search_setpos' => 'mb_ereg_search_setpos',
'mberegi' => 'mb_eregi',
'mberegi_replace' => 'mb_eregi_replace',
'mbregex_encoding' => 'mb_regex_encoding',
'mbsplit' => 'mb_split',
],
'@openssl' => [
'openssl_get_publickey' => 'openssl_pkey_get_public',
'openssl_get_privatekey' => 'openssl_pkey_get_private',
],
'@sodium' => [
'sodium_crypto_scalarmult_base' => 'sodium_crypto_box_publickey_from_secretkey',
],
'@exif' => [
'read_exif_data' => 'exif_read_data',
],
'@ftp' => [
'ftp_quit' => 'ftp_close',
],
'@posix' => [
'posix_errno' => 'posix_get_last_error',
],
'@pcntl' => [
'pcntl_errno' => 'pcntl_get_last_error',
],
'@time' => [
'mktime' => ['time', 0],
'gmmktime' => ['time', 0],
],
];
/**
* @var array<string, array<int|string>|string> stores alias (key) - master (value) functions mapping
*/
private array $aliases = [];
public function configure(array $configuration): void
{
parent::configure($configuration);
$this->aliases = [];
foreach ($this->configuration['sets'] as $set) {
if ('@all' === $set) {
$this->aliases = array_merge(...array_values(self::SETS));
break;
}
$this->aliases = array_merge($this->aliases, self::SETS[$set]);
}
}
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Master functions shall be used instead of aliases.',
[
new CodeSample(
'<?php
$a = chop($b);
close($b);
$a = doubleval($b);
$a = fputs($b, $c);
$a = get_required_files();
ini_alter($b, $c);
$a = is_double($b);
$a = is_integer($b);
$a = is_long($b);
$a = is_real($b);
$a = is_writeable($b);
$a = join($glue, $pieces);
$a = key_exists($key, $array);
magic_quotes_runtime($new_setting);
$a = pos($array);
$a = show_source($filename, true);
$a = sizeof($b);
$a = strchr($haystack, $needle);
$a = imap_header($imap_stream, 1);
user_error($message);
mbereg_search_getregs();
'
),
new CodeSample(
'<?php
$a = is_double($b);
mbereg_search_getregs();
',
['sets' => ['@mbreg']]
),
],
null,
'Risky when any of the alias functions are overridden.'
);
}
/**
* {@inheritdoc}
*
* Must run before ImplodeCallFixer, PhpUnitDedicateAssertFixer.
*/
public function getPriority(): int
{
return 40;
}
/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(T_STRING);
}
/**
* {@inheritdoc}
*/
public function isRisky(): bool
{
return true;
}
/**
* {@inheritdoc}
*/
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$functionsAnalyzer = new FunctionsAnalyzer();
$argumentsAnalyzer = new ArgumentsAnalyzer();
/** @var Token $token */
foreach ($tokens->findGivenKind(T_STRING) as $index => $token) {
// check mapping hit
$tokenContent = strtolower($token->getContent());
if (!isset($this->aliases[$tokenContent])) {
continue;
}
// skip expressions without parameters list
$openParenthesis = $tokens->getNextMeaningfulToken($index);
if (!$tokens[$openParenthesis]->equals('(')) {
continue;
}
if (!$functionsAnalyzer->isGlobalFunctionCall($tokens, $index)) {
continue;
}
if (\is_array($this->aliases[$tokenContent])) {
[$alias, $numberOfArguments] = $this->aliases[$tokenContent];
$count = $argumentsAnalyzer->countArguments($tokens, $openParenthesis, $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openParenthesis));
if ($numberOfArguments !== $count) {
continue;
}
} else {
$alias = $this->aliases[$tokenContent];
}
$tokens[$index] = new Token([T_STRING, $alias]);
}
}
/**
* {@inheritdoc}
*/
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
$sets = [
'@all' => 'all listed sets',
'@internal' => 'native functions',
'@exif' => 'EXIF functions',
'@ftp' => 'FTP functions',
'@IMAP' => 'IMAP functions',
'@ldap' => 'LDAP functions',
'@mbreg' => 'from `ext-mbstring`',
'@mysqli' => 'mysqli functions',
'@oci' => 'oci functions',
'@odbc' => 'odbc functions',
'@openssl' => 'openssl functions',
'@pcntl' => 'PCNTL functions',
'@pg' => 'pg functions',
'@posix' => 'POSIX functions',
'@snmp' => 'SNMP functions', // @TODO Remove on next major 4.0 as this set is now empty
'@sodium' => 'libsodium functions',
'@time' => 'time functions',
];
$list = "List of sets to fix. Defined sets are:\n\n";
foreach ($sets as $set => $description) {
$list .= sprintf("* `%s` (%s)\n", $set, $description);
}
return new FixerConfigurationResolver([
(new FixerOptionBuilder('sets', $list))
->setAllowedTypes(['array'])
->setAllowedValues([new AllowedValueSubset(array_keys($sets))])
->setDefault(['@internal', '@IMAP', '@pg'])
->getOption(),
]);
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\Alias;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
final class NoAliasLanguageConstructCallFixer extends AbstractFixer
{
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Master language constructs shall be used instead of aliases.',
[
new CodeSample(
'<?php
die;
'
),
]
);
}
/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(T_EXIT);
}
/**
* {@inheritdoc}
*/
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
foreach ($tokens as $index => $token) {
if (!$token->isGivenKind(T_EXIT)) {
continue;
}
if ('exit' === strtolower($token->getContent())) {
continue;
}
$tokens[$index] = new Token([T_EXIT, 'exit']);
}
}
}
@@ -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\Fixer\Alias;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Sullivan Senechal <soullivaneuh@gmail.com>
*/
final class NoMixedEchoPrintFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/**
* @var string
*/
private $callBack;
/**
* @var int T_ECHO or T_PRINT
*/
private $candidateTokenType;
/**
* {@inheritdoc}
*/
public function configure(array $configuration): void
{
parent::configure($configuration);
if ('echo' === $this->configuration['use']) {
$this->candidateTokenType = T_PRINT;
$this->callBack = 'fixPrintToEcho';
} else {
$this->candidateTokenType = T_ECHO;
$this->callBack = 'fixEchoToPrint';
}
}
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Either language construct `print` or `echo` should be used.',
[
new CodeSample("<?php print 'example';\n"),
new CodeSample("<?php echo('example');\n", ['use' => 'print']),
]
);
}
/**
* {@inheritdoc}
*
* Must run after EchoTagSyntaxFixer.
*/
public function getPriority(): int
{
return -10;
}
/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound($this->candidateTokenType);
}
/**
* {@inheritdoc}
*/
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$callBack = $this->callBack;
foreach ($tokens as $index => $token) {
if ($token->isGivenKind($this->candidateTokenType)) {
$this->{$callBack}($tokens, $index);
}
}
}
/**
* {@inheritdoc}
*/
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('use', 'The desired language construct.'))
->setAllowedValues(['print', 'echo'])
->setDefault('echo')
->getOption(),
]);
}
private function fixEchoToPrint(Tokens $tokens, int $index): void
{
$nextTokenIndex = $tokens->getNextMeaningfulToken($index);
$endTokenIndex = $tokens->getNextTokenOfKind($index, [';', [T_CLOSE_TAG]]);
$canBeConverted = true;
for ($i = $nextTokenIndex; $i < $endTokenIndex; ++$i) {
if ($tokens[$i]->equalsAny(['(', [CT::T_ARRAY_SQUARE_BRACE_OPEN]])) {
$blockType = Tokens::detectBlockType($tokens[$i]);
$i = $tokens->findBlockEnd($blockType['type'], $i);
}
if ($tokens[$i]->equals(',')) {
$canBeConverted = false;
break;
}
}
if (false === $canBeConverted) {
return;
}
$tokens[$index] = new Token([T_PRINT, 'print']);
}
private function fixPrintToEcho(Tokens $tokens, int $index): void
{
$prevToken = $tokens[$tokens->getPrevMeaningfulToken($index)];
if (!$prevToken->equalsAny([';', '{', '}', ')', [T_OPEN_TAG], [T_ELSE]])) {
return;
}
$tokens[$index] = new Token([T_ECHO, 'echo']);
}
}
@@ -0,0 +1,230 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\Alias;
use PhpCsFixer\AbstractFunctionReferenceFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Analyzer\ArgumentsAnalyzer;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
final class PowToExponentiationFixer extends AbstractFunctionReferenceFixer
{
/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens): bool
{
// minimal candidate to fix is seven tokens: pow(x,y);
return $tokens->count() > 7 && $tokens->isTokenKindFound(T_STRING);
}
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Converts `pow` to the `**` operator.',
[
new CodeSample(
"<?php\n pow(\$a, 1);\n"
),
],
null,
'Risky when the function `pow` is overridden.'
);
}
/**
* {@inheritdoc}
*
* Must run before BinaryOperatorSpacesFixer, MethodArgumentSpaceFixer, NativeFunctionCasingFixer, NoSpacesAfterFunctionNameFixer, NoSpacesInsideParenthesisFixer.
*/
public function getPriority(): int
{
return 32;
}
/**
* {@inheritdoc}
*/
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$candidates = $this->findPowCalls($tokens);
$argumentsAnalyzer = new ArgumentsAnalyzer();
$numberOfTokensAdded = 0;
$previousCloseParenthesisIndex = \count($tokens);
foreach (array_reverse($candidates) as $candidate) {
// if in the previous iteration(s) tokens were added to the collection and this is done within the tokens
// indices of the current candidate than the index of the close ')' of the candidate has moved and so
// the index needs to be updated
if ($previousCloseParenthesisIndex < $candidate[2]) {
$previousCloseParenthesisIndex = $candidate[2];
$candidate[2] += $numberOfTokensAdded;
} else {
$previousCloseParenthesisIndex = $candidate[2];
$numberOfTokensAdded = 0;
}
$arguments = $argumentsAnalyzer->getArguments($tokens, $candidate[1], $candidate[2]);
if (2 !== \count($arguments)) {
continue;
}
for ($i = $candidate[1]; $i < $candidate[2]; ++$i) {
if ($tokens[$i]->isGivenKind(T_ELLIPSIS)) {
continue 2;
}
}
$numberOfTokensAdded += $this->fixPowToExponentiation(
$tokens,
$candidate[0], // functionNameIndex,
$candidate[1], // openParenthesisIndex,
$candidate[2], // closeParenthesisIndex,
$arguments
);
}
}
/**
* @return array<int[]>
*/
private function findPowCalls(Tokens $tokens): array
{
$candidates = [];
// Minimal candidate to fix is seven tokens: pow(x,y);
$end = \count($tokens) - 6;
// First possible location is after the open token: 1
for ($i = 1; $i < $end; ++$i) {
$candidate = $this->find('pow', $tokens, $i, $end);
if (null === $candidate) {
break;
}
$i = $candidate[1]; // proceed to openParenthesisIndex
$candidates[] = $candidate;
}
return $candidates;
}
/**
* @param array<int, int> $arguments
*
* @return int number of tokens added to the collection
*/
private function fixPowToExponentiation(Tokens $tokens, int $functionNameIndex, int $openParenthesisIndex, int $closeParenthesisIndex, array $arguments): int
{
// find the argument separator ',' directly after the last token of the first argument;
// replace it with T_POW '**'
$tokens[$tokens->getNextTokenOfKind(reset($arguments), [','])] = new Token([T_POW, '**']);
// clean up the function call tokens prt. I
$tokens->clearAt($closeParenthesisIndex);
$previousIndex = $tokens->getPrevMeaningfulToken($closeParenthesisIndex);
if ($tokens[$previousIndex]->equals(',')) {
$tokens->clearAt($previousIndex); // trailing ',' in function call (PHP 7.3)
}
$added = 0;
// check if the arguments need to be wrapped in parentheses
foreach (array_reverse($arguments, true) as $argumentStartIndex => $argumentEndIndex) {
if ($this->isParenthesisNeeded($tokens, $argumentStartIndex, $argumentEndIndex)) {
$tokens->insertAt($argumentEndIndex + 1, new Token(')'));
$tokens->insertAt($argumentStartIndex, new Token('('));
$added += 2;
}
}
// clean up the function call tokens prt. II
$tokens->clearAt($openParenthesisIndex);
$tokens->clearAt($functionNameIndex);
$prevMeaningfulTokenIndex = $tokens->getPrevMeaningfulToken($functionNameIndex);
if ($tokens[$prevMeaningfulTokenIndex]->isGivenKind(T_NS_SEPARATOR)) {
$tokens->clearAt($prevMeaningfulTokenIndex);
}
return $added;
}
private function isParenthesisNeeded(Tokens $tokens, int $argumentStartIndex, int $argumentEndIndex): bool
{
static $allowedKinds = null;
if (null === $allowedKinds) {
$allowedKinds = $this->getAllowedKinds();
}
for ($i = $argumentStartIndex; $i <= $argumentEndIndex; ++$i) {
if ($tokens[$i]->isGivenKind($allowedKinds) || $tokens->isEmptyAt($i)) {
continue;
}
$blockType = Tokens::detectBlockType($tokens[$i]);
if (null !== $blockType) {
$i = $tokens->findBlockEnd($blockType['type'], $i);
continue;
}
if ($tokens[$i]->equals('$')) {
$i = $tokens->getNextMeaningfulToken($i);
if ($tokens[$i]->isGivenKind(CT::T_DYNAMIC_VAR_BRACE_OPEN)) {
$i = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_DYNAMIC_VAR_BRACE, $i);
continue;
}
}
if ($tokens[$i]->equals('+') && $tokens->getPrevMeaningfulToken($i) < $argumentStartIndex) {
continue;
}
return true;
}
return false;
}
/**
* @return int[]
*/
private function getAllowedKinds(): array
{
return array_merge(
[
T_DNUMBER, T_LNUMBER, T_VARIABLE, T_STRING, T_CONSTANT_ENCAPSED_STRING, T_DOUBLE_CAST,
T_INT_CAST, T_INC, T_DEC, T_NS_SEPARATOR, T_WHITESPACE, T_DOUBLE_COLON, T_LINE, T_COMMENT, T_DOC_COMMENT,
CT::T_NAMESPACE_OPERATOR,
],
Token::getObjectOperatorKinds()
);
}
}
@@ -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\Fixer\Alias;
use PhpCsFixer\AbstractFunctionReferenceFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Analyzer\ArgumentsAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
/**
* @author Vladimir Reznichenko <kalessil@gmail.com>
*/
final class RandomApiMigrationFixer extends AbstractFunctionReferenceFixer implements ConfigurableFixerInterface
{
/**
* @var array<string, array<int, int>>
*/
private static array $argumentCounts = [
'getrandmax' => [0],
'mt_rand' => [1, 2],
'rand' => [0, 2],
'srand' => [0, 1],
'random_int' => [0, 2],
];
/**
* {@inheritdoc}
*/
public function configure(array $configuration): void
{
parent::configure($configuration);
foreach ($this->configuration['replacements'] as $functionName => $replacement) {
$this->configuration['replacements'][$functionName] = [
'alternativeName' => $replacement,
'argumentCount' => self::$argumentCounts[$functionName],
];
}
}
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Replaces `rand`, `srand`, `getrandmax` functions calls with their `mt_*` analogs or `random_int`.',
[
new CodeSample("<?php\n\$a = getrandmax();\n\$a = rand(\$b, \$c);\n\$a = srand();\n"),
new CodeSample(
"<?php\n\$a = getrandmax();\n\$a = rand(\$b, \$c);\n\$a = srand();\n",
['replacements' => ['getrandmax' => 'mt_getrandmax']]
),
new CodeSample(
"<?php \$a = rand(\$b, \$c);\n",
['replacements' => ['rand' => 'random_int']]
),
],
null,
'Risky when the configured functions are overridden. Or when relying on the seed based generating of the numbers.'
);
}
/**
* {@inheritdoc}
*/
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$argumentsAnalyzer = new ArgumentsAnalyzer();
foreach ($this->configuration['replacements'] as $functionIdentity => $functionReplacement) {
if ($functionIdentity === $functionReplacement['alternativeName']) {
continue;
}
$currIndex = 0;
do {
// try getting function reference and translate boundaries for humans
$boundaries = $this->find($functionIdentity, $tokens, $currIndex, $tokens->count() - 1);
if (null === $boundaries) {
// next function search, as current one not found
continue 2;
}
[$functionName, $openParenthesis, $closeParenthesis] = $boundaries;
$count = $argumentsAnalyzer->countArguments($tokens, $openParenthesis, $closeParenthesis);
if (!\in_array($count, $functionReplacement['argumentCount'], true)) {
continue 2;
}
// analysing cursor shift, so nested calls could be processed
$currIndex = $openParenthesis;
$tokens[$functionName] = new Token([T_STRING, $functionReplacement['alternativeName']]);
if (0 === $count && 'random_int' === $functionReplacement['alternativeName']) {
$tokens->insertAt($currIndex + 1, [
new Token([T_LNUMBER, '0']),
new Token(','),
new Token([T_WHITESPACE, ' ']),
new Token([T_STRING, 'getrandmax']),
new Token('('),
new Token(')'),
]);
$currIndex += 6;
}
} while (null !== $currIndex);
}
}
/**
* {@inheritdoc}
*/
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('replacements', 'Mapping between replaced functions with the new ones.'))
->setAllowedTypes(['array'])
->setAllowedValues([static function (array $value): bool {
foreach ($value as $functionName => $replacement) {
if (!\array_key_exists($functionName, self::$argumentCounts)) {
throw new InvalidOptionsException(sprintf(
'Function "%s" is not handled by the fixer.',
$functionName
));
}
if (!\is_string($replacement)) {
throw new InvalidOptionsException(sprintf(
'Replacement for function "%s" must be a string, "%s" given.',
$functionName,
get_debug_type($replacement)
));
}
}
return true;
}])
->setDefault([
'getrandmax' => 'mt_getrandmax',
'rand' => 'mt_rand', // @TODO change to `random_int` as default on 4.0
'srand' => 'mt_srand',
])
->getOption(),
]);
}
}
@@ -0,0 +1,249 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\Alias;
use PhpCsFixer\AbstractFunctionReferenceFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Analyzer\ArgumentsAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
final class SetTypeToCastFixer extends AbstractFunctionReferenceFixer
{
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Cast shall be used, not `settype`.',
[
new CodeSample(
'<?php
settype($foo, "integer");
settype($bar, "string");
settype($bar, "null");
'
),
],
null,
'Risky when the `settype` function is overridden or when used as the 2nd or 3rd expression in a `for` loop .'
);
}
/**
* {@inheritdoc}
*
* Must run after NoBinaryStringFixer, NoUselessConcatOperatorFixer.
*/
public function getPriority(): int
{
return 0;
}
/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isAllTokenKindsFound([T_CONSTANT_ENCAPSED_STRING, T_STRING, T_VARIABLE]);
}
/**
* {@inheritdoc}
*/
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$map = [
'array' => [T_ARRAY_CAST, '(array)'],
'bool' => [T_BOOL_CAST, '(bool)'],
'boolean' => [T_BOOL_CAST, '(bool)'],
'double' => [T_DOUBLE_CAST, '(float)'],
'float' => [T_DOUBLE_CAST, '(float)'],
'int' => [T_INT_CAST, '(int)'],
'integer' => [T_INT_CAST, '(int)'],
'object' => [T_OBJECT_CAST, '(object)'],
'string' => [T_STRING_CAST, '(string)'],
// note: `'null' is dealt with later on
];
$argumentsAnalyzer = new ArgumentsAnalyzer();
foreach (array_reverse($this->findSettypeCalls($tokens)) as $candidate) {
$functionNameIndex = $candidate[0];
$arguments = $argumentsAnalyzer->getArguments($tokens, $candidate[1], $candidate[2]);
if (2 !== \count($arguments)) {
continue; // function must be overridden or used incorrectly
}
$prev = $tokens->getPrevMeaningfulToken($functionNameIndex);
if (!$tokens[$prev]->equalsAny([';', '{', '}', [T_OPEN_TAG]])) {
continue; // return value of the function is used
}
reset($arguments);
// --- Test first argument --------------------
$firstArgumentStart = key($arguments);
if ($tokens[$firstArgumentStart]->isComment() || $tokens[$firstArgumentStart]->isWhitespace()) {
$firstArgumentStart = $tokens->getNextMeaningfulToken($firstArgumentStart);
}
if (!$tokens[$firstArgumentStart]->isGivenKind(T_VARIABLE)) {
continue; // settype only works with variables pass by reference, function must be overridden
}
$commaIndex = $tokens->getNextMeaningfulToken($firstArgumentStart);
if (null === $commaIndex || !$tokens[$commaIndex]->equals(',')) {
continue; // first argument is complex statement; function must be overridden
}
// --- Test second argument -------------------
next($arguments);
$secondArgumentStart = key($arguments);
$secondArgumentEnd = $arguments[$secondArgumentStart];
if ($tokens[$secondArgumentStart]->isComment() || $tokens[$secondArgumentStart]->isWhitespace()) {
$secondArgumentStart = $tokens->getNextMeaningfulToken($secondArgumentStart);
}
if (
!$tokens[$secondArgumentStart]->isGivenKind(T_CONSTANT_ENCAPSED_STRING)
|| $tokens->getNextMeaningfulToken($secondArgumentStart) < $secondArgumentEnd
) {
continue; // second argument is of the wrong type or is a (complex) statement of some sort (function is overridden)
}
// --- Test type ------------------------------
$type = strtolower(trim($tokens[$secondArgumentStart]->getContent(), '"\'"'));
if ('null' !== $type && !isset($map[$type])) {
continue; // we don't know how to map
}
// --- Fixing ---------------------------------
$argumentToken = $tokens[$firstArgumentStart];
$this->removeSettypeCall(
$tokens,
$functionNameIndex,
$candidate[1],
$firstArgumentStart,
$commaIndex,
$secondArgumentStart,
$candidate[2]
);
if ('null' === $type) {
$this->fixSettypeNullCall($tokens, $functionNameIndex, $argumentToken);
} else {
$this->fixSettypeCall($tokens, $functionNameIndex, $argumentToken, new Token($map[$type]));
}
}
}
/**
* @return list<list<int>>
*/
private function findSettypeCalls(Tokens $tokens): array
{
$candidates = [];
$end = \count($tokens);
for ($i = 1; $i < $end; ++$i) {
$candidate = $this->find('settype', $tokens, $i, $end);
if (null === $candidate) {
break;
}
$i = $candidate[1]; // proceed to openParenthesisIndex
$candidates[] = $candidate;
}
return $candidates;
}
private function removeSettypeCall(
Tokens $tokens,
int $functionNameIndex,
int $openParenthesisIndex,
int $firstArgumentStart,
int $commaIndex,
int $secondArgumentStart,
int $closeParenthesisIndex
): void {
$tokens->clearTokenAndMergeSurroundingWhitespace($closeParenthesisIndex);
$prevIndex = $tokens->getPrevMeaningfulToken($closeParenthesisIndex);
if ($tokens[$prevIndex]->equals(',')) {
$tokens->clearTokenAndMergeSurroundingWhitespace($prevIndex);
}
$tokens->clearTokenAndMergeSurroundingWhitespace($secondArgumentStart);
$tokens->clearTokenAndMergeSurroundingWhitespace($commaIndex);
$tokens->clearTokenAndMergeSurroundingWhitespace($firstArgumentStart);
$tokens->clearTokenAndMergeSurroundingWhitespace($openParenthesisIndex);
$tokens->clearAt($functionNameIndex); // we'll be inserting here so no need to merge the space tokens
$tokens->clearEmptyTokens();
}
private function fixSettypeCall(
Tokens $tokens,
int $functionNameIndex,
Token $argumentToken,
Token $castToken
): void {
$tokens->insertAt(
$functionNameIndex,
[
clone $argumentToken,
new Token([T_WHITESPACE, ' ']),
new Token('='),
new Token([T_WHITESPACE, ' ']),
$castToken,
new Token([T_WHITESPACE, ' ']),
clone $argumentToken,
]
);
$tokens->removeTrailingWhitespace($functionNameIndex + 6); // 6 = number of inserted tokens -1 for offset correction
}
private function fixSettypeNullCall(
Tokens $tokens,
int $functionNameIndex,
Token $argumentToken
): void {
$tokens->insertAt(
$functionNameIndex,
[
clone $argumentToken,
new Token([T_WHITESPACE, ' ']),
new Token('='),
new Token([T_WHITESPACE, ' ']),
new Token([T_STRING, 'null']),
]
);
$tokens->removeTrailingWhitespace($functionNameIndex + 4); // 4 = number of inserted tokens -1 for offset correction
}
}
@@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\ArrayNotation;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Gregor Harlan <gharlan@web.de>
* @author Sebastiaan Stok <s.stok@rollerscapes.net>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
*/
final class ArraySyntaxFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/**
* @var null|int
*/
private $candidateTokenKind;
/**
* @var null|string
*/
private $fixCallback;
/**
* {@inheritdoc}
*/
public function configure(array $configuration): void
{
parent::configure($configuration);
$this->resolveCandidateTokenKind();
$this->resolveFixCallback();
}
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'PHP arrays should be declared using the configured syntax.',
[
new CodeSample(
"<?php\narray(1,2);\n"
),
new CodeSample(
"<?php\n[1,2];\n",
['syntax' => 'long']
),
]
);
}
/**
* {@inheritdoc}
*
* Must run before BinaryOperatorSpacesFixer, TernaryOperatorSpacesFixer.
*/
public function getPriority(): int
{
return 1;
}
/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound($this->candidateTokenKind);
}
/**
* {@inheritdoc}
*/
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
$callback = $this->fixCallback;
for ($index = $tokens->count() - 1; 0 <= $index; --$index) {
if ($tokens[$index]->isGivenKind($this->candidateTokenKind)) {
$this->{$callback}($tokens, $index);
}
}
}
/**
* {@inheritdoc}
*/
protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('syntax', 'Whether to use the `long` or `short` array syntax.'))
->setAllowedValues(['long', 'short'])
->setDefault('short')
->getOption(),
]);
}
private function fixToLongArraySyntax(Tokens $tokens, int $index): void
{
$closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, $index);
$tokens[$index] = new Token('(');
$tokens[$closeIndex] = new Token(')');
$tokens->insertAt($index, new Token([T_ARRAY, 'array']));
}
private function fixToShortArraySyntax(Tokens $tokens, int $index): void
{
$openIndex = $tokens->getNextTokenOfKind($index, ['(']);
$closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openIndex);
$tokens[$openIndex] = new Token([CT::T_ARRAY_SQUARE_BRACE_OPEN, '[']);
$tokens[$closeIndex] = new Token([CT::T_ARRAY_SQUARE_BRACE_CLOSE, ']']);
$tokens->clearTokenAndMergeSurroundingWhitespace($index);
}
private function resolveFixCallback(): void
{
$this->fixCallback = sprintf('fixTo%sArraySyntax', ucfirst($this->configuration['syntax']));
}
private function resolveCandidateTokenKind(): void
{
$this->candidateTokenKind = 'long' === $this->configuration['syntax'] ? CT::T_ARRAY_SQUARE_BRACE_OPEN : T_ARRAY;
}
}
@@ -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\Fixer\ArrayNotation;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
/**
* @author Carlos Cirello <carlos.cirello.nl@gmail.com>
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
* @author Graham Campbell <hello@gjcampbell.co.uk>
*/
final class NoMultilineWhitespaceAroundDoubleArrowFixer extends AbstractFixer
{
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Operator `=>` should not be surrounded by multi-line whitespaces.',
[new CodeSample("<?php\n\$a = array(1\n\n=> 2);\n")]
);
}
/**
* {@inheritdoc}
*
* Must run before BinaryOperatorSpacesFixer, MethodArgumentSpaceFixer, TrailingCommaInMultilineFixer.
*/
public function getPriority(): int
{
return 31;
}
/**
* {@inheritdoc}
*/
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(T_DOUBLE_ARROW);
}
/**
* {@inheritdoc}
*/
protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
foreach ($tokens as $index => $token) {
if (!$token->isGivenKind(T_DOUBLE_ARROW)) {
continue;
}
if (!$tokens[$index - 2]->isComment() || str_starts_with($tokens[$index - 2]->getContent(), '/*')) {
$this->fixWhitespace($tokens, $index - 1);
}
// do not move anything about if there is a comment following the whitespace
if (!$tokens[$index + 2]->isComment()) {
$this->fixWhitespace($tokens, $index + 1);
}
}
}
private function fixWhitespace(Tokens $tokens, int $index): void
{
$token = $tokens[$index];
if ($token->isWhitespace() && !$token->isWhitespace(" \t")) {
$tokens[$index] = new Token([T_WHITESPACE, rtrim($token->getContent()).' ']);
}
}
}
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <fabien@symfony.com>
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace PhpCsFixer\Fixer\ArrayNotation;
use PhpCsFixer\AbstractProxyFixer;
use PhpCsFixer\Fixer\Basic\NoTrailingCommaInSinglelineFixer;
use PhpCsFixer\Fixer\DeprecatedFixerInterface;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
/**
* @deprecated
*
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
* @author Sebastiaan Stok <s.stok@rollerscapes.net>
*/
final class NoTrailingCommaInSinglelineArrayFixer extends AbstractProxyFixer implements DeprecatedFixerInterface
{
/**
* {@inheritdoc}
*/
public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'PHP single-line arrays should not have trailing comma.',
[new CodeSample("<?php\n\$a = array('sample', );\n")]
);
}
/**
* {@inheritdoc}
*/
public function getSuccessorsNames(): array
{
return array_keys($this->proxyFixers);
}
/**
* {@inheritdoc}
*/
protected function createProxyFixers(): array
{
$fixer = new NoTrailingCommaInSinglelineFixer();
$fixer->configure(['elements' => ['array']]);
return [$fixer];
}
}

Some files were not shown because too many files have changed in this diff Show More