155 lines
3.2 KiB
PHP
155 lines
3.2 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
if (
|
|
!(is_file($file = __DIR__ . '/../vendor/autoload.php') && include $file) &&
|
|
!(is_file($file = __DIR__ . '/../../../autoload.php') && include $file)
|
|
) {
|
|
fwrite(STDERR, "Install packages using Composer.\n");
|
|
exit(1);
|
|
}
|
|
|
|
|
|
echo '
|
|
NEON linter
|
|
-----------
|
|
';
|
|
|
|
if ($argc < 2) {
|
|
echo "Usage: neon-lint [--debug] <path>\n";
|
|
exit(1);
|
|
}
|
|
|
|
$debug = in_array('--debug', $argv, true);
|
|
if ($debug) {
|
|
echo "Debug mode\n";
|
|
}
|
|
|
|
$path = end($argv);
|
|
|
|
try {
|
|
$linter = new NeonLinter(debug: $debug);
|
|
$ok = $linter->scanDirectory($path);
|
|
exit($ok ? 0 : 1);
|
|
|
|
} catch (Throwable $e) {
|
|
fwrite(STDERR, $debug ? "\n$e\n" : "\nError: {$e->getMessage()}\n");
|
|
exit(2);
|
|
}
|
|
|
|
|
|
class NeonLinter
|
|
{
|
|
/** @var string[] */
|
|
public array $excludedDirs = ['.*', '*.tmp', 'temp', 'vendor', 'node_modules'];
|
|
|
|
|
|
public function __construct(
|
|
private readonly bool $debug = false,
|
|
) {
|
|
}
|
|
|
|
|
|
public function scanDirectory(string $path): bool
|
|
{
|
|
$this->initialize();
|
|
echo "Scanning $path\n";
|
|
$counter = 0;
|
|
$errors = 0;
|
|
foreach ($this->getFiles($path) as $file) {
|
|
$file = (string) $file;
|
|
echo preg_replace('~\.?[/\\\]~A', '', $file), "\x0D";
|
|
$errors += $this->lintFile($file) ? 0 : 1;
|
|
echo str_pad('...', strlen($file)), "\x0D";
|
|
$counter++;
|
|
}
|
|
|
|
echo "Done (checked $counter files, found errors in $errors)\n";
|
|
return !$errors;
|
|
}
|
|
|
|
|
|
public function lintFile(string $file): bool
|
|
{
|
|
if ($this->debug) {
|
|
echo $file, "\n";
|
|
}
|
|
|
|
$s = file_get_contents($file);
|
|
if (str_starts_with($s, "\xEF\xBB\xBF")) {
|
|
$this->writeError('WARNING', $file, 'contains BOM');
|
|
$s = substr($s, 3);
|
|
}
|
|
|
|
try {
|
|
Nette\Neon\Neon::decode($s);
|
|
return true;
|
|
|
|
} catch (Nette\Neon\Exception $e) {
|
|
if ($this->debug) {
|
|
echo $e;
|
|
}
|
|
$this->writeError('ERROR', $file, $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
private function initialize(): void
|
|
{
|
|
if (function_exists('pcntl_signal')) {
|
|
pcntl_signal(SIGINT, function (): never {
|
|
pcntl_signal(SIGINT, SIG_DFL);
|
|
echo "Terminated\n";
|
|
exit(1);
|
|
});
|
|
} elseif (function_exists('sapi_windows_set_ctrl_handler')) {
|
|
sapi_windows_set_ctrl_handler(function (): never {
|
|
echo "Terminated\n";
|
|
exit(1);
|
|
});
|
|
}
|
|
|
|
set_time_limit(0);
|
|
}
|
|
|
|
|
|
private function getFiles(string $path): Iterator
|
|
{
|
|
$it = match (true) {
|
|
is_file($path) => new ArrayIterator([$path]),
|
|
is_dir($path) => $this->findNeonFiles($path),
|
|
(bool) preg_match('~[*?]~', $path) => new GlobIterator($path),
|
|
default => throw new InvalidArgumentException("File or directory '$path' not found."),
|
|
};
|
|
return new CallbackFilterIterator($it, fn($file) => is_file((string) $file));
|
|
}
|
|
|
|
|
|
private function findNeonFiles(string $dir): Generator
|
|
{
|
|
foreach (scandir($dir) as $name) {
|
|
$path = ($dir === '.' ? '' : $dir . DIRECTORY_SEPARATOR) . $name;
|
|
if ($name !== '.' && $name !== '..' && is_dir($path)) {
|
|
foreach ($this->excludedDirs as $pattern) {
|
|
if (fnmatch($pattern, $name)) {
|
|
continue 2;
|
|
}
|
|
}
|
|
yield from $this->findNeonFiles($path);
|
|
|
|
} elseif (str_ends_with($name, '.neon')) {
|
|
yield $path;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private function writeError(string $label, string $file, string $message): void
|
|
{
|
|
fwrite(STDERR, str_pad("[$label]", 13) . ' ' . $file . ' ' . $message . "\n");
|
|
}
|
|
}
|