301 lines
9.2 KiB
PHP
301 lines
9.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/*
|
|
* This file is part of PHP CS Fixer.
|
|
*
|
|
* (c) Fabien Potencier <fabien@symfony.com>
|
|
* Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
|
*
|
|
* This source file is subject to the MIT license that is bundled
|
|
* with this source code in the file LICENSE.
|
|
*/
|
|
|
|
namespace PhpCsFixer\Runner;
|
|
|
|
use PhpCsFixer\AbstractFixer;
|
|
use PhpCsFixer\Cache\CacheManagerInterface;
|
|
use PhpCsFixer\Cache\Directory;
|
|
use PhpCsFixer\Cache\DirectoryInterface;
|
|
use PhpCsFixer\Differ\DifferInterface;
|
|
use PhpCsFixer\Error\Error;
|
|
use PhpCsFixer\Error\ErrorsManager;
|
|
use PhpCsFixer\FileReader;
|
|
use PhpCsFixer\Fixer\FixerInterface;
|
|
use PhpCsFixer\FixerFileProcessedEvent;
|
|
use PhpCsFixer\Linter\LinterInterface;
|
|
use PhpCsFixer\Linter\LintingException;
|
|
use PhpCsFixer\Linter\LintingResultInterface;
|
|
use PhpCsFixer\Tokenizer\Tokens;
|
|
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
|
use Symfony\Component\Filesystem\Exception\IOException;
|
|
use Symfony\Contracts\EventDispatcher\Event;
|
|
|
|
/**
|
|
* @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
|
|
*/
|
|
final class Runner
|
|
{
|
|
private DifferInterface $differ;
|
|
|
|
private ?DirectoryInterface $directory;
|
|
|
|
private ?EventDispatcherInterface $eventDispatcher;
|
|
|
|
private ErrorsManager $errorsManager;
|
|
|
|
private CacheManagerInterface $cacheManager;
|
|
|
|
private bool $isDryRun;
|
|
|
|
private LinterInterface $linter;
|
|
|
|
/**
|
|
* @var \Traversable<\SplFileInfo>
|
|
*/
|
|
private $finder;
|
|
|
|
/**
|
|
* @var list<FixerInterface>
|
|
*/
|
|
private array $fixers;
|
|
|
|
private bool $stopOnViolation;
|
|
|
|
/**
|
|
* @param \Traversable<\SplFileInfo> $finder
|
|
* @param list<FixerInterface> $fixers
|
|
*/
|
|
public function __construct(
|
|
\Traversable $finder,
|
|
array $fixers,
|
|
DifferInterface $differ,
|
|
?EventDispatcherInterface $eventDispatcher,
|
|
ErrorsManager $errorsManager,
|
|
LinterInterface $linter,
|
|
bool $isDryRun,
|
|
CacheManagerInterface $cacheManager,
|
|
?DirectoryInterface $directory = null,
|
|
bool $stopOnViolation = false
|
|
) {
|
|
$this->finder = $finder;
|
|
$this->fixers = $fixers;
|
|
$this->differ = $differ;
|
|
$this->eventDispatcher = $eventDispatcher;
|
|
$this->errorsManager = $errorsManager;
|
|
$this->linter = $linter;
|
|
$this->isDryRun = $isDryRun;
|
|
$this->cacheManager = $cacheManager;
|
|
$this->directory = $directory ?: new Directory('');
|
|
$this->stopOnViolation = $stopOnViolation;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, array{appliedFixers: list<string>, diff: string}>
|
|
*/
|
|
public function fix(): array
|
|
{
|
|
$changed = [];
|
|
|
|
$finder = $this->finder;
|
|
$finderIterator = $finder instanceof \IteratorAggregate ? $finder->getIterator() : $finder;
|
|
$fileFilteredFileIterator = new FileFilterIterator(
|
|
$finderIterator,
|
|
$this->eventDispatcher,
|
|
$this->cacheManager
|
|
);
|
|
|
|
$collection = $this->linter->isAsync()
|
|
? new FileCachingLintingIterator($fileFilteredFileIterator, $this->linter)
|
|
: new FileLintingIterator($fileFilteredFileIterator, $this->linter);
|
|
|
|
foreach ($collection as $file) {
|
|
$fixInfo = $this->fixFile($file, $collection->currentLintingResult());
|
|
|
|
// we do not need Tokens to still caching just fixed file - so clear the cache
|
|
Tokens::clearCache();
|
|
|
|
if (null !== $fixInfo) {
|
|
$name = $this->directory->getRelativePathTo($file->__toString());
|
|
$changed[$name] = $fixInfo;
|
|
|
|
if ($this->stopOnViolation) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $changed;
|
|
}
|
|
|
|
/**
|
|
* @return null|array{appliedFixers: list<string>, diff: string}
|
|
*/
|
|
private function fixFile(\SplFileInfo $file, LintingResultInterface $lintingResult): ?array
|
|
{
|
|
$name = $file->getPathname();
|
|
|
|
try {
|
|
$lintingResult->check();
|
|
} catch (LintingException $e) {
|
|
$this->dispatchEvent(
|
|
FixerFileProcessedEvent::NAME,
|
|
new FixerFileProcessedEvent(FixerFileProcessedEvent::STATUS_INVALID)
|
|
);
|
|
|
|
$this->errorsManager->report(new Error(Error::TYPE_INVALID, $name, $e));
|
|
|
|
return null;
|
|
}
|
|
|
|
$old = FileReader::createSingleton()->read($file->getRealPath());
|
|
|
|
$tokens = Tokens::fromCode($old);
|
|
$oldHash = $tokens->getCodeHash();
|
|
|
|
$newHash = $oldHash;
|
|
$new = $old;
|
|
|
|
$appliedFixers = [];
|
|
|
|
try {
|
|
foreach ($this->fixers as $fixer) {
|
|
// for custom fixers we don't know is it safe to run `->fix()` without checking `->supports()` and `->isCandidate()`,
|
|
// thus we need to check it and conditionally skip fixing
|
|
if (
|
|
!$fixer instanceof AbstractFixer
|
|
&& (!$fixer->supports($file) || !$fixer->isCandidate($tokens))
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
$fixer->fix($file, $tokens);
|
|
|
|
if ($tokens->isChanged()) {
|
|
$tokens->clearEmptyTokens();
|
|
$tokens->clearChanged();
|
|
$appliedFixers[] = $fixer->getName();
|
|
}
|
|
}
|
|
} catch (\ParseError $e) {
|
|
$this->dispatchEvent(
|
|
FixerFileProcessedEvent::NAME,
|
|
new FixerFileProcessedEvent(FixerFileProcessedEvent::STATUS_LINT)
|
|
);
|
|
|
|
$this->errorsManager->report(new Error(Error::TYPE_LINT, $name, $e));
|
|
|
|
return null;
|
|
} catch (\Throwable $e) {
|
|
$this->processException($name, $e);
|
|
|
|
return null;
|
|
}
|
|
|
|
$fixInfo = null;
|
|
|
|
if (!empty($appliedFixers)) {
|
|
$new = $tokens->generateCode();
|
|
$newHash = $tokens->getCodeHash();
|
|
}
|
|
|
|
// We need to check if content was changed and then applied changes.
|
|
// But we can't simply check $appliedFixers, because one fixer may revert
|
|
// work of other and both of them will mark collection as changed.
|
|
// Therefore we need to check if code hashes changed.
|
|
if ($oldHash !== $newHash) {
|
|
$fixInfo = [
|
|
'appliedFixers' => $appliedFixers,
|
|
'diff' => $this->differ->diff($old, $new, $file),
|
|
];
|
|
|
|
try {
|
|
$this->linter->lintSource($new)->check();
|
|
} catch (LintingException $e) {
|
|
$this->dispatchEvent(
|
|
FixerFileProcessedEvent::NAME,
|
|
new FixerFileProcessedEvent(FixerFileProcessedEvent::STATUS_LINT)
|
|
);
|
|
|
|
$this->errorsManager->report(new Error(Error::TYPE_LINT, $name, $e, $fixInfo['appliedFixers'], $fixInfo['diff']));
|
|
|
|
return null;
|
|
}
|
|
|
|
if (!$this->isDryRun) {
|
|
$fileName = $file->getRealPath();
|
|
|
|
if (!file_exists($fileName)) {
|
|
throw new IOException(
|
|
sprintf('Failed to write file "%s" (no longer) exists.', $file->getPathname()),
|
|
0,
|
|
null,
|
|
$file->getPathname()
|
|
);
|
|
}
|
|
|
|
if (is_dir($fileName)) {
|
|
throw new IOException(
|
|
sprintf('Cannot write file "%s" as the location exists as directory.', $fileName),
|
|
0,
|
|
null,
|
|
$fileName
|
|
);
|
|
}
|
|
|
|
if (!is_writable($fileName)) {
|
|
throw new IOException(
|
|
sprintf('Cannot write to file "%s" as it is not writable.', $fileName),
|
|
0,
|
|
null,
|
|
$fileName
|
|
);
|
|
}
|
|
|
|
if (false === @file_put_contents($fileName, $new)) {
|
|
$error = error_get_last();
|
|
|
|
throw new IOException(
|
|
sprintf('Failed to write file "%s", "%s".', $fileName, $error ? $error['message'] : 'no reason available'),
|
|
0,
|
|
null,
|
|
$fileName
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->cacheManager->setFile($name, $new);
|
|
|
|
$this->dispatchEvent(
|
|
FixerFileProcessedEvent::NAME,
|
|
new FixerFileProcessedEvent($fixInfo ? FixerFileProcessedEvent::STATUS_FIXED : FixerFileProcessedEvent::STATUS_NO_CHANGES)
|
|
);
|
|
|
|
return $fixInfo;
|
|
}
|
|
|
|
/**
|
|
* Process an exception that occurred.
|
|
*/
|
|
private function processException(string $name, \Throwable $e): void
|
|
{
|
|
$this->dispatchEvent(
|
|
FixerFileProcessedEvent::NAME,
|
|
new FixerFileProcessedEvent(FixerFileProcessedEvent::STATUS_EXCEPTION)
|
|
);
|
|
|
|
$this->errorsManager->report(new Error(Error::TYPE_EXCEPTION, $name, $e));
|
|
}
|
|
|
|
private function dispatchEvent(string $name, Event $event): void
|
|
{
|
|
if (null === $this->eventDispatcher) {
|
|
return;
|
|
}
|
|
|
|
$this->eventDispatcher->dispatch($event, $name);
|
|
}
|
|
}
|