Missing dependancies
This commit is contained in:
+238
@@ -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;
|
||||
}
|
||||
}
|
||||
+80
@@ -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)];
|
||||
}
|
||||
}
|
||||
+120
@@ -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;
|
||||
}
|
||||
}
|
||||
+225
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
+36
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
+45
@@ -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;
|
||||
}
|
||||
}
|
||||
+24
@@ -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
|
||||
{
|
||||
}
|
||||
+24
@@ -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()];
|
||||
}
|
||||
}
|
||||
+428
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
Vendored
+46
@@ -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;
|
||||
}
|
||||
}
|
||||
+128
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
Vendored
+51
@@ -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;
|
||||
}
|
||||
}
|
||||
+131
@@ -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).']';
|
||||
}
|
||||
}
|
||||
+96
@@ -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;
|
||||
}
|
||||
}
|
||||
+93
@@ -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;
|
||||
}
|
||||
}
|
||||
+180
@@ -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;
|
||||
}
|
||||
}
|
||||
+961
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
+133
@@ -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)));
|
||||
}
|
||||
}
|
||||
+23
@@ -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;
|
||||
}
|
||||
Vendored
+71
@@ -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;
|
||||
}
|
||||
}
|
||||
+61
@@ -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;
|
||||
}
|
||||
}
|
||||
+67
@@ -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;
|
||||
}
|
||||
}
|
||||
+141
@@ -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;
|
||||
}
|
||||
}
|
||||
+92
@@ -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;
|
||||
}
|
||||
}
|
||||
+92
@@ -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];
|
||||
}
|
||||
}
|
||||
Vendored
+30
@@ -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;
|
||||
}
|
||||
+99
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
+129
@@ -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;
|
||||
}
|
||||
}
|
||||
Vendored
+58
@@ -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);
|
||||
}
|
||||
}
|
||||
Vendored
+46
@@ -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;
|
||||
}
|
||||
}
|
||||
Vendored
+89
@@ -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];
|
||||
}
|
||||
}
|
||||
Vendored
+30
@@ -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;
|
||||
}
|
||||
Vendored
+57
@@ -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;
|
||||
}
|
||||
}
|
||||
+54
@@ -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;
|
||||
}
|
||||
}
|
||||
+31
@@ -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;
|
||||
}
|
||||
+110
@@ -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);
|
||||
}
|
||||
}
|
||||
Vendored
+37
@@ -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;
|
||||
}
|
||||
}
|
||||
+82
@@ -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';
|
||||
}
|
||||
}
|
||||
+368
@@ -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;
|
||||
}
|
||||
}
|
||||
+174
@@ -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);
|
||||
}
|
||||
}
|
||||
Vendored
+104
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
;
|
||||
}
|
||||
}
|
||||
+58
@@ -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;
|
||||
}
|
||||
}
|
||||
+159
@@ -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);
|
||||
}
|
||||
}
|
||||
+205
@@ -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);
|
||||
}
|
||||
}
|
||||
+139
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+233
@@ -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);
|
||||
}
|
||||
}
|
||||
+335
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Vendored
+68
@@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
+154
@@ -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']);
|
||||
}
|
||||
}
|
||||
+230
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
+170
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
+249
@@ -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
|
||||
}
|
||||
}
|
||||
+151
@@ -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;
|
||||
}
|
||||
}
|
||||
+89
@@ -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()).' ']);
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
@@ -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
Reference in New Issue
Block a user