1 | // Copyright (c) 2019-2023 Alexander Medvednikov. All rights reserved. |
2 | // Use of this source code is governed by an MIT license |
3 | // that can be found in the LICENSE file. |
4 | module main |
5 | |
6 | import os |
7 | import os.cmdline |
8 | import rand |
9 | import term |
10 | import v.ast |
11 | import v.pref |
12 | import v.fmt |
13 | import v.util |
14 | import v.util.diff |
15 | import v.parser |
16 | import v.help |
17 | |
18 | struct FormatOptions { |
19 | is_l bool |
20 | is_c bool // Note: This refers to the '-c' fmt flag, NOT the C backend |
21 | is_w bool |
22 | is_diff bool |
23 | is_verbose bool |
24 | is_all bool |
25 | is_debug bool |
26 | is_noerror bool |
27 | is_verify bool // exit(1) if the file is not vfmt'ed |
28 | is_worker bool // true *only* in the worker processes. Note: workers can crash. |
29 | is_backup bool // make a `file.v.bak` copy *before* overwriting a `file.v` in place with `-w` |
30 | mut: |
31 | diff_cmd string // filled in when -diff or -verify is passed |
32 | } |
33 | |
34 | const ( |
35 | formatted_file_token = '\@\@\@' + 'FORMATTED_FILE: ' |
36 | vtmp_folder = os.vtmp_dir() |
37 | term_colors = term.can_show_color_on_stderr() |
38 | ) |
39 | |
40 | fn main() { |
41 | // if os.getenv('VFMT_ENABLE') == '' { |
42 | // eprintln('v fmt is disabled for now') |
43 | // exit(1) |
44 | // } |
45 | toolexe := os.executable() |
46 | util.set_vroot_folder(os.dir(os.dir(os.dir(toolexe)))) |
47 | args := util.join_env_vflags_and_os_args() |
48 | mut foptions := FormatOptions{ |
49 | is_c: '-c' in args |
50 | is_l: '-l' in args |
51 | is_w: '-w' in args |
52 | is_diff: '-diff' in args |
53 | is_verbose: '-verbose' in args || '--verbose' in args |
54 | is_all: '-all' in args || '--all' in args |
55 | is_worker: '-worker' in args |
56 | is_debug: '-debug' in args |
57 | is_noerror: '-noerror' in args |
58 | is_verify: '-verify' in args |
59 | is_backup: '-backup' in args |
60 | } |
61 | if term_colors { |
62 | os.setenv('VCOLORS', 'always', true) |
63 | } |
64 | foptions.vlog('vfmt foptions: ${foptions}') |
65 | if foptions.is_worker { |
66 | // -worker should be added by a parent vfmt process. |
67 | // We launch a sub process for each file because |
68 | // the v compiler can do an early exit if it detects |
69 | // a syntax error, but we want to process ALL passed |
70 | // files if possible. |
71 | foptions.format_file(cmdline.option(args, '-worker', '')) |
72 | exit(0) |
73 | } |
74 | // we are NOT a worker at this stage, i.e. we are a parent vfmt process |
75 | possible_files := cmdline.only_non_options(cmdline.options_after(args, ['fmt'])) |
76 | if foptions.is_verbose { |
77 | eprintln('vfmt toolexe: ${toolexe}') |
78 | eprintln('vfmt args: ' + os.args.str()) |
79 | eprintln('vfmt env_vflags_and_os_args: ' + args.str()) |
80 | eprintln('vfmt possible_files: ' + possible_files.str()) |
81 | } |
82 | files := util.find_all_v_files(possible_files) or { |
83 | verror(err.msg()) |
84 | return |
85 | } |
86 | if os.is_atty(0) == 0 && files.len == 0 { |
87 | foptions.format_pipe() |
88 | exit(0) |
89 | } |
90 | if files.len == 0 || '-help' in args || '--help' in args { |
91 | help.print_and_exit('fmt') |
92 | exit(0) |
93 | } |
94 | mut cli_args_no_files := []string{} |
95 | for idx, a in os.args { |
96 | if idx == 0 { |
97 | cli_args_no_files << os.quoted_path(a) |
98 | continue |
99 | } |
100 | if a !in files { |
101 | cli_args_no_files << a |
102 | } |
103 | } |
104 | mut errors := 0 |
105 | for file in files { |
106 | fpath := os.real_path(file) |
107 | mut worker_command_array := cli_args_no_files.clone() |
108 | worker_command_array << ['-worker', util.quote_path(fpath)] |
109 | worker_cmd := worker_command_array.join(' ') |
110 | foptions.vlog('vfmt worker_cmd: ${worker_cmd}') |
111 | worker_result := os.execute(worker_cmd) |
112 | // Guard against a possibly crashing worker process. |
113 | if worker_result.exit_code != 0 { |
114 | eprintln(worker_result.output) |
115 | if worker_result.exit_code == 1 { |
116 | eprintln('Internal vfmt error while formatting file: ${file}.') |
117 | } |
118 | errors++ |
119 | continue |
120 | } |
121 | if worker_result.output.len > 0 { |
122 | if worker_result.output.contains(formatted_file_token) { |
123 | wresult := worker_result.output.split(formatted_file_token) |
124 | formatted_warn_errs := wresult[0] |
125 | formatted_file_path := wresult[1].trim_right('\n\r') |
126 | foptions.post_process_file(fpath, formatted_file_path) or { errors = errors + 1 } |
127 | if formatted_warn_errs.len > 0 { |
128 | eprintln(formatted_warn_errs) |
129 | } |
130 | continue |
131 | } |
132 | } |
133 | errors++ |
134 | } |
135 | if errors > 0 { |
136 | eprintln('Encountered a total of: ${errors} errors.') |
137 | if foptions.is_noerror { |
138 | exit(0) |
139 | } |
140 | if foptions.is_verify { |
141 | exit(1) |
142 | } |
143 | if foptions.is_c { |
144 | exit(2) |
145 | } |
146 | exit(1) |
147 | } |
148 | } |
149 | |
150 | fn setup_preferences_and_table() (&pref.Preferences, &ast.Table) { |
151 | table := ast.new_table() |
152 | mut prefs := pref.new_preferences() |
153 | prefs.is_fmt = true |
154 | prefs.skip_warnings = true |
155 | return prefs, table |
156 | } |
157 | |
158 | fn (foptions &FormatOptions) vlog(msg string) { |
159 | if foptions.is_verbose { |
160 | eprintln(msg) |
161 | } |
162 | } |
163 | |
164 | fn (foptions &FormatOptions) format_file(file string) { |
165 | foptions.vlog('vfmt2 running fmt.fmt over file: ${file}') |
166 | prefs, table := setup_preferences_and_table() |
167 | file_ast := parser.parse_file(file, table, .parse_comments, prefs) |
168 | // checker.new_checker(table, prefs).check(file_ast) |
169 | formatted_content := fmt.fmt(file_ast, table, prefs, foptions.is_debug) |
170 | file_name := os.file_name(file) |
171 | ulid := rand.ulid() |
172 | vfmt_output_path := os.join_path(vtmp_folder, 'vfmt_${ulid}_${file_name}') |
173 | os.write_file(vfmt_output_path, formatted_content) or { panic(err) } |
174 | foptions.vlog('fmt.fmt worked and ${formatted_content.len} bytes were written to ${vfmt_output_path} .') |
175 | eprintln('${formatted_file_token}${vfmt_output_path}') |
176 | } |
177 | |
178 | fn (foptions &FormatOptions) format_pipe() { |
179 | foptions.vlog('vfmt2 running fmt.fmt over stdin') |
180 | prefs, table := setup_preferences_and_table() |
181 | input_text := os.get_raw_lines_joined() |
182 | file_ast := parser.parse_text(input_text, '', table, .parse_comments, prefs) |
183 | // checker.new_checker(table, prefs).check(file_ast) |
184 | formatted_content := fmt.fmt(file_ast, table, prefs, foptions.is_debug, source_text: input_text) |
185 | print(formatted_content) |
186 | flush_stdout() |
187 | foptions.vlog('fmt.fmt worked and ${formatted_content.len} bytes were written to stdout.') |
188 | } |
189 | |
190 | fn print_compiler_options(compiler_params &pref.Preferences) { |
191 | eprintln(' os: ' + compiler_params.os.str()) |
192 | eprintln(' ccompiler: ${compiler_params.ccompiler}') |
193 | eprintln(' path: ${compiler_params.path} ') |
194 | eprintln(' out_name: ${compiler_params.out_name} ') |
195 | eprintln(' vroot: ${compiler_params.vroot} ') |
196 | eprintln('lookup_path: ${compiler_params.lookup_path} ') |
197 | eprintln(' out_name: ${compiler_params.out_name} ') |
198 | eprintln(' cflags: ${compiler_params.cflags} ') |
199 | eprintln(' is_test: ${compiler_params.is_test} ') |
200 | eprintln(' is_script: ${compiler_params.is_script} ') |
201 | } |
202 | |
203 | fn (mut foptions FormatOptions) find_diff_cmd() string { |
204 | if foptions.diff_cmd != '' { |
205 | return foptions.diff_cmd |
206 | } |
207 | if foptions.is_verify || foptions.is_diff { |
208 | foptions.diff_cmd = diff.find_working_diff_command() or { |
209 | eprintln(err) |
210 | exit(1) |
211 | } |
212 | } |
213 | return foptions.diff_cmd |
214 | } |
215 | |
216 | fn (mut foptions FormatOptions) post_process_file(file string, formatted_file_path string) ! { |
217 | if formatted_file_path.len == 0 { |
218 | return |
219 | } |
220 | fc := os.read_file(file) or { |
221 | eprintln('File ${file} could not be read') |
222 | return |
223 | } |
224 | formatted_fc := os.read_file(formatted_file_path) or { |
225 | eprintln('File ${formatted_file_path} could not be read') |
226 | return |
227 | } |
228 | is_formatted_different := fc != formatted_fc |
229 | if foptions.is_diff { |
230 | if !is_formatted_different { |
231 | return |
232 | } |
233 | diff_cmd := foptions.find_diff_cmd() |
234 | foptions.vlog('Using diff command: ${diff_cmd}') |
235 | diff_ := diff.color_compare_files(diff_cmd, file, formatted_file_path) |
236 | if diff_.len > 0 { |
237 | println(diff_) |
238 | } |
239 | return |
240 | } |
241 | if foptions.is_verify { |
242 | if !is_formatted_different { |
243 | return |
244 | } |
245 | println("${file} is not vfmt'ed") |
246 | return error('') |
247 | } |
248 | if foptions.is_c { |
249 | if is_formatted_different { |
250 | eprintln('File is not formatted: ${file}') |
251 | return error('') |
252 | } |
253 | return |
254 | } |
255 | if foptions.is_l { |
256 | if is_formatted_different { |
257 | eprintln('File needs formatting: ${file}') |
258 | } |
259 | return |
260 | } |
261 | if foptions.is_w { |
262 | if is_formatted_different { |
263 | if foptions.is_backup { |
264 | file_bak := '${file}.bak' |
265 | os.cp(file, file_bak) or {} |
266 | } |
267 | mut perms_to_restore := u32(0) |
268 | $if !windows { |
269 | fm := os.inode(file) |
270 | perms_to_restore = fm.bitmask() |
271 | } |
272 | os.mv_by_cp(formatted_file_path, file) or { panic(err) } |
273 | $if !windows { |
274 | os.chmod(file, int(perms_to_restore)) or { panic(err) } |
275 | } |
276 | eprintln('Reformatted file: ${file}') |
277 | } else { |
278 | eprintln('Already formatted file: ${file}') |
279 | } |
280 | return |
281 | } |
282 | print(formatted_fc) |
283 | flush_stdout() |
284 | } |
285 | |
286 | fn (f FormatOptions) str() string { |
287 | return |
288 | 'FormatOptions{ is_l: ${f.is_l}, is_w: ${f.is_w}, is_diff: ${f.is_diff}, is_verbose: ${f.is_verbose},' + |
289 | ' is_all: ${f.is_all}, is_worker: ${f.is_worker}, is_debug: ${f.is_debug}, is_noerror: ${f.is_noerror},' + |
290 | ' is_verify: ${f.is_verify}" }' |
291 | } |
292 | |
293 | fn file_to_mod_name_and_is_module_file(file string) (string, bool) { |
294 | mut mod_name := 'main' |
295 | mut is_module_file := false |
296 | flines := read_source_lines(file) or { return mod_name, is_module_file } |
297 | for fline in flines { |
298 | line := fline.trim_space() |
299 | if line.starts_with('module ') { |
300 | if !line.starts_with('module main') { |
301 | is_module_file = true |
302 | mod_name = line.replace('module ', ' ').trim_space() |
303 | } |
304 | break |
305 | } |
306 | } |
307 | return mod_name, is_module_file |
308 | } |
309 | |
310 | fn read_source_lines(file string) ![]string { |
311 | source_lines := os.read_lines(file) or { return error('can not read ${file}') } |
312 | return source_lines |
313 | } |
314 | |
315 | fn get_compile_name_of_potential_v_project(file string) string { |
316 | // This function get_compile_name_of_potential_v_project returns: |
317 | // a) the file's folder, if file is part of a v project |
318 | // b) the file itself, if the file is a standalone v program |
319 | pfolder := os.real_path(os.dir(file)) |
320 | // a .v project has many 'module main' files in one folder |
321 | // if there is only one .v file, then it must be a standalone |
322 | all_files_in_pfolder := os.ls(pfolder) or { panic(err) } |
323 | mut vfiles := []string{} |
324 | for f in all_files_in_pfolder { |
325 | vf := os.join_path(pfolder, f) |
326 | if f.starts_with('.') || !f.ends_with('.v') || os.is_dir(vf) { |
327 | continue |
328 | } |
329 | vfiles << vf |
330 | } |
331 | if vfiles.len == 1 { |
332 | return file |
333 | } |
334 | // ///////////////////////////////////////////////////////////// |
335 | // At this point, we know there are many .v files in the folder |
336 | // We will have to read them all, and if there are more than one |
337 | // containing `fn main` then the folder contains multiple standalone |
338 | // v programs. If only one contains `fn main` then the folder is |
339 | // a project folder, that should be compiled with `v pfolder`. |
340 | mut main_fns := 0 |
341 | for f in vfiles { |
342 | slines := read_source_lines(f) or { panic(err) } |
343 | for line in slines { |
344 | if line.contains('fn main()') { |
345 | main_fns++ |
346 | if main_fns > 1 { |
347 | return file |
348 | } |
349 | } |
350 | } |
351 | } |
352 | return pfolder |
353 | } |
354 | |
355 | [noreturn] |
356 | fn verror(s string) { |
357 | util.verror('vfmt error', s) |
358 | } |