#!/usr/bin/env php \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"); } }