Skip to content

Instantly share code, notes, and snippets.

@staabm
Last active December 10, 2023 08:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save staabm/ec764d9476bbefe55fcda7744951ef62 to your computer and use it in GitHub Desktop.
Save staabm/ec764d9476bbefe55fcda7744951ef62 to your computer and use it in GitHub Desktop.
fast-lint
<?php
declare(strict_types=1);
/*
* This file is part of the overtrue/phplint package
*
* (c) overtrue
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Overtrue\PHPLint;
use Fiber;
use LogicException;
use Overtrue\PHPLint\Configuration\OptionDefinition;
use Overtrue\PHPLint\Configuration\Resolver;
use Overtrue\PHPLint\Event\AfterCheckingEvent;
use Overtrue\PHPLint\Event\AfterLintFileEvent;
use Overtrue\PHPLint\Event\BeforeCheckingEvent;
use Overtrue\PHPLint\Event\BeforeLintFileEvent;
use Overtrue\PHPLint\Output\LinterOutput;
use Overtrue\PHPLint\Process\LintProcess;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Adapter\NullAdapter;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use Throwable;
use function array_push;
use function array_slice;
use function count;
use function md5_file;
use function microtime;
use function phpversion;
use function version_compare;
/**
* @author Overtrue
* @author Laurent Laville (code-rewrites since v9.0)
*/
final class Linter
{
private Resolver $configResolver;
private EventDispatcherInterface $dispatcher;
private Cache $cache;
private array $results;
private int $processLimit;
private string $memoryLimit;
private bool $warning;
private array $options;
private string $appLongVersion;
public function __construct(Resolver $configResolver, EventDispatcherInterface $dispatcher, string $appVersion = '9.1.x-dev')
{
$this->configResolver = $configResolver;
$this->dispatcher = $dispatcher;
$this->appLongVersion = $appVersion;
$this->options = $configResolver->getOptions();
$this->processLimit = $configResolver->getOption(OptionDefinition::JOBS);
$this->memoryLimit = (string) $configResolver->getOption(OptionDefinition::OPTION_MEMORY_LIMIT);
$this->warning = $configResolver->getOption(OptionDefinition::WARNING);
if ($configResolver->getOption(OptionDefinition::NO_CACHE)) {
$adapter = new NullAdapter();
} else {
$adapter = new FilesystemAdapter('paths', 0, $configResolver->getOption(OptionDefinition::CACHE));
}
$this->cache = new Cache($adapter);
$this->results = [
'errors' => [],
'warnings' => [],
'hits' => [],
'misses' => [],
];
}
/**
* @throws Throwable
*/
public function lintFiles(Finder $finder, ?float $startTime = null): LinterOutput
{
if (null === $startTime) {
$startTime = microtime(true);
}
try {
$fileCount = count($finder);
} catch (LogicException) {
$fileCount = 0;
}
$this->dispatcher->dispatch(
new BeforeCheckingEvent(
$this,
['fileCount' => $fileCount, 'appVersion' => $this->appLongVersion, 'options' => $this->options]
)
);
$processCount = 0;
if ($fileCount > 0) {
$iterator = $finder->getIterator();
while ($iterator->valid()) {
$fileInfo = $iterator->current();
$this->dispatcher->dispatch(new BeforeLintFileEvent($this, ['file' => $fileInfo]));
$filename = $fileInfo->getRealPath();
if ($this->cache->isHit($filename)) {
$this->results['hits'][] = $fileInfo;
} else {
$this->results['misses'][] = $fileInfo;
}
$iterator->next();
}
if (version_compare(phpversion(), '8.3', 'ge')) {
$chunkSize = 10;
} else {
$chunkSize = 1;
}
$chunks = array_chunk($this->results['misses'], $chunkSize);
$processRunning = [];
$pid = 0;
foreach($chunks as $chunk) {
$lintProcess = $this->createLintProcess($chunk);
$lintProcess->start();
++$pid;
$processRunning[$pid] = [
'process' => $lintProcess,
'files' => $chunk,
];
while (count($processRunning) >= $this->processLimit) {
foreach ($processRunning as $pid => $item) {
/** @var LintProcess $lintProcess */
$lintProcess = $item['process'];
if ($lintProcess->isRunning()) {
continue;
}
unset($processRunning[$pid]);
// checks status of all files linked at end of the php lint process
foreach ($item['files'] as $fileInfo) {
$status = $this->processFile($fileInfo, $lintProcess);
$this->dispatcher->dispatch(
new AfterLintFileEvent($this, ['file' => $fileInfo, 'status' => $status])
);
}
}
}
}
$results = $this->results;
} else {
$results = [];
}
$finalResults = new LinterOutput($results, $finder);
$finalResults->setContext($this->configResolver, $startTime, $processCount);
$this->dispatcher->dispatch(new AfterCheckingEvent($this, ['results' => $finalResults]));
return $finalResults;
}
private function processFile(SplFileInfo $fileInfo, LintProcess $lintProcess): string
{
$filename = $fileInfo->getRealPath();
$item = $lintProcess->getItem($fileInfo);
if ($item->hasSyntaxError()) {
$status = 'error';
} elseif ($this->warning && $item->hasSyntaxWarning()) {
$status = 'warning';
} else {
$status = 'ok';
$item = $this->cache->getItem($filename);
$item->set(md5_file($filename));
$this->cache->saveItem($item);
}
if ($status !== 'ok') {
$this->results[$status . 's'][$filename] = [
'absolute_file' => $filename,
'relative_file' => $item->getFileInfo()->getRelativePathname(),
'error' => $item->getMessage(),
'line' => $item->getLine(),
];
}
return $status;
}
private function createLintProcess(array $files): LintProcess
{
$command = [
PHP_SAPI == 'cli' ? PHP_BINARY : PHP_BINDIR . '/php',
'-d error_reporting=E_ALL',
'-d display_errors=On',
];
if (!empty($this->memoryLimit)) {
$command[] = '-d memory_limit=' . $this->memoryLimit;
}
$command[] = '-l';
array_push($command, ...$files);
return (new LintProcess($command))->setFiles($files);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment