Skip to content

Commit 5163bfd

Browse files
committed
improved neon-lint
1 parent 50b955c commit 5163bfd

File tree

1 file changed

+114
-52
lines changed

1 file changed

+114
-52
lines changed

bin/neon-lint

Lines changed: 114 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -11,82 +11,144 @@ if (
1111
exit(1);
1212
}
1313

14-
if (function_exists('pcntl_signal')) {
15-
pcntl_signal(SIGINT, function (): void {
16-
pcntl_signal(SIGINT, SIG_DFL);
17-
echo "Terminated\n";
18-
exit(1);
19-
});
20-
} elseif (function_exists('sapi_windows_set_ctrl_handler')) {
21-
sapi_windows_set_ctrl_handler(function () {
22-
echo "Terminated\n";
23-
exit(1);
24-
});
25-
}
26-
27-
set_time_limit(0);
28-
2914

3015
echo '
3116
NEON linter
3217
-----------
3318
';
3419

3520
if ($argc < 2) {
36-
echo "Usage: neon-lint <path>\n";
21+
echo "Usage: neon-lint [--debug] <path>\n";
3722
exit(1);
3823
}
3924

40-
$ok = scanPath($argv[1]);
41-
exit($ok ? 0 : 1);
25+
$debug = in_array('--debug', $argv, true);
26+
if ($debug) {
27+
echo "Debug mode\n";
28+
}
29+
30+
$path = end($argv);
31+
32+
try {
33+
$linter = new NeonLinter(debug: $debug);
34+
$ok = $linter->scanDirectory($path);
35+
exit($ok ? 0 : 1);
36+
37+
} catch (Throwable $e) {
38+
fwrite(STDERR, $debug ? "\n$e\n" : "\nError: {$e->getMessage()}\n");
39+
exit(2);
40+
}
4241

4342

44-
function scanPath(string $path): bool
43+
class NeonLinter
4544
{
46-
echo "Scanning $path\n";
45+
/** @var string[] */
46+
public array $excludedDirs = ['.*', '*.tmp', 'temp', 'vendor', 'node_modules'];
4747

48-
$it = new RecursiveDirectoryIterator($path);
49-
$it = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::LEAVES_ONLY);
50-
$it = new RegexIterator($it, '~\.neon$~');
5148

52-
$counter = 0;
53-
$success = true;
54-
foreach ($it as $file) {
55-
echo str_pad(str_repeat('.', $counter++ % 40), 40), "\x0D";
56-
$success = lintFile((string) $file) && $success;
49+
public function __construct(
50+
private readonly bool $debug = false,
51+
) {
5752
}
5853

59-
echo str_pad('', 40), "\x0D";
60-
echo "Done.\n";
61-
return $success;
62-
}
6354

55+
public function scanDirectory(string $path): bool
56+
{
57+
$this->initialize();
58+
echo "Scanning $path\n";
59+
$counter = 0;
60+
$errors = 0;
61+
foreach ($this->getFiles($path) as $file) {
62+
$file = (string) $file;
63+
echo preg_replace('~\.?[/\\\]~A', '', $file), "\x0D";
64+
$errors += $this->lintFile($file) ? 0 : 1;
65+
echo str_pad('...', strlen($file)), "\x0D";
66+
$counter++;
67+
}
6468

65-
function lintFile(string $file): bool
66-
{
67-
set_error_handler(function (int $severity, string $message) use ($file) {
68-
if ($severity === E_USER_DEPRECATED) {
69-
fwrite(STDERR, "[DEPRECATED] $file $message\n");
70-
return null;
69+
echo "Done (checked $counter files, found errors in $errors)\n";
70+
return !$errors;
71+
}
72+
73+
74+
public function lintFile(string $file): bool
75+
{
76+
if ($this->debug) {
77+
echo $file, "\n";
7178
}
72-
return false;
73-
});
7479

75-
$s = file_get_contents($file);
76-
if (substr($s, 0, 3) === "\xEF\xBB\xBF") {
77-
fwrite(STDERR, "[WARNING] $file contains BOM\n");
78-
$contents = substr($s, 3);
80+
$s = file_get_contents($file);
81+
if (str_starts_with($s, "\xEF\xBB\xBF")) {
82+
$this->writeError('WARNING', $file, 'contains BOM');
83+
$s = substr($s, 3);
84+
}
85+
86+
try {
87+
Nette\Neon\Neon::decode($s);
88+
return true;
89+
90+
} catch (Nette\Neon\Exception $e) {
91+
if ($this->debug) {
92+
echo $e;
93+
}
94+
$this->writeError('ERROR', $file, $e->getMessage());
95+
return false;
96+
}
97+
}
98+
99+
100+
private function initialize(): void
101+
{
102+
if (function_exists('pcntl_signal')) {
103+
pcntl_signal(SIGINT, function (): never {
104+
pcntl_signal(SIGINT, SIG_DFL);
105+
echo "Terminated\n";
106+
exit(1);
107+
});
108+
} elseif (function_exists('sapi_windows_set_ctrl_handler')) {
109+
sapi_windows_set_ctrl_handler(function (): never {
110+
echo "Terminated\n";
111+
exit(1);
112+
});
113+
}
114+
115+
set_time_limit(0);
116+
}
117+
118+
119+
private function getFiles(string $path): Iterator
120+
{
121+
$it = match (true) {
122+
is_file($path) => new ArrayIterator([$path]),
123+
is_dir($path) => $this->findNeonFiles($path),
124+
(bool) preg_match('~[*?]~', $path) => new GlobIterator($path),
125+
default => throw new InvalidArgumentException("File or directory '$path' not found."),
126+
};
127+
return new CallbackFilterIterator($it, fn($file) => is_file((string) $file));
79128
}
80129

81-
try {
82-
Nette\Neon\Neon::decode($s);
83-
return true;
84130

85-
} catch (Nette\Neon\Exception $e) {
86-
fwrite(STDERR, "[ERROR] $file {$e->getMessage()}\n");
131+
private function findNeonFiles(string $dir): Generator
132+
{
133+
foreach (scandir($dir) as $name) {
134+
$path = ($dir === '.' ? '' : $dir . DIRECTORY_SEPARATOR) . $name;
135+
if ($name !== '.' && $name !== '..' && is_dir($path)) {
136+
foreach ($this->excludedDirs as $pattern) {
137+
if (fnmatch($pattern, $name)) {
138+
continue 2;
139+
}
140+
}
141+
yield from $this->findNeonFiles($path);
142+
143+
} elseif (str_ends_with($name, '.neon')) {
144+
yield $path;
145+
}
146+
}
147+
}
148+
87149

88-
} finally {
89-
restore_error_handler();
150+
private function writeError(string $label, string $file, string $message): void
151+
{
152+
fwrite(STDERR, str_pad("[$label]", 13) . ' ' . $file . ' ' . $message . "\n");
90153
}
91-
return false;
92154
}

0 commit comments

Comments
 (0)