@@ -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
3015echo '
3116NEON linter
3217-----------
3318 ' ;
3419
3520if ($ 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