v / cmd / tools / vfmt.v
342 lines · 327 sloc · 10.14 KB · 6d796798eb8923f5c72ffb9ee953208183c30246
Raw
1// Copyright (c) 2019-2024 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.
4module main
5
6import os
7import os.cmdline
8import rand
9import term
10import v.ast
11import v.pref
12import v.fmt
13import v.util
14import v.util.diff
15import v.parser
16import v.help
17
18struct 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_debug bool
25 is_noerror bool
26 is_verify bool // exit(1) if the file is not vfmt'ed
27 is_worker bool // true *only* in the worker processes. Note: workers can crash.
28 is_backup bool // make a `file.v.bak` copy *before* overwriting a `file.v` in place with `-w`
29 in_process bool // do not fork a worker process; potentially faster, but more prone to crashes for invalid files
30 is_new_int bool // Forcefully cast the `int` type in @[translated] modules or in the definition of `C.func` to the `i32` type.
31mut:
32 diff_cmd string // filled in when -diff or -verify is passed
33}
34
35const formatted_file_token = '\@\@\@' + 'FORMATTED_FILE: '
36const vtmp_folder = os.vtmp_dir()
37const term_colors = term.can_show_color_on_stderr()
38const vfmt_only_flags = ['-backup', '-c', '-diff', '-inprocess', '-l', '-new_int', '-noerror',
39 '-verbose', '--verbose', '-verify', '-w']
40
41fn main() {
42 // if os.getenv('VFMT_ENABLE') == '' {
43 // eprintln('v fmt is disabled for now')
44 // exit(1)
45 // }
46 toolexe := os.executable()
47 util.set_vroot_folder(os.dir(os.dir(os.dir(toolexe))))
48 args := util.join_env_vflags_and_os_args()
49 mut foptions := FormatOptions{
50 is_c: '-c' in args
51 is_l: '-l' in args
52 is_w: '-w' in args
53 is_diff: '-diff' in args
54 is_verbose: '-verbose' in args || '--verbose' 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 in_process: '-inprocess' in args
61 is_new_int: '-new_int' in args
62 }
63 if term_colors {
64 os.setenv('VCOLORS', 'always', true)
65 }
66 foptions.vlog('vfmt foptions: ${foptions}')
67 if foptions.is_worker {
68 // -worker should be added by a parent vfmt process.
69 // We launch a sub process for each file because
70 // the v compiler can do an early exit if it detects
71 // a syntax error, but we want to process ALL passed
72 // files if possible.
73 foptions.format_file(cmdline.option(args, '-worker', ''))
74 exit(0)
75 }
76 // we are NOT a worker at this stage, i.e. we are a parent vfmt process
77 possible_files := cmdline.only_non_options(cmdline.options_after(args, ['fmt']))
78 if foptions.is_verbose {
79 eprintln('vfmt toolexe: ${toolexe}')
80 eprintln('vfmt args: ' + os.args.str())
81 eprintln('vfmt env_vflags_and_os_args: ' + args.str())
82 eprintln('vfmt possible_files: ' + possible_files.str())
83 }
84 if '-help' in args || '--help' in args {
85 help.print_and_exit('fmt')
86 }
87 files := util.find_all_v_files(possible_files) or {
88 verror(err.msg())
89 return
90 }
91 if os.is_atty(0) == 0 && files.len == 0 {
92 foptions.format_pipe()
93 exit(0)
94 }
95 if files.len == 0 {
96 help.print_and_exit('fmt')
97 }
98 mut cli_args_no_files := []string{}
99 for idx, a in os.args {
100 if idx == 0 {
101 cli_args_no_files << os.quoted_path(a)
102 continue
103 }
104 if a !in files {
105 cli_args_no_files << a
106 }
107 }
108 mut errors := 0
109 mut has_internal_error := false
110 mut prefs := setup_preferences(args)
111 for file in files {
112 fpath := os.real_path(file)
113 if foptions.is_verify && foptions.in_process {
114 // For a small amount of files, it is faster to process
115 // everything directly in the same process, single threaded,
116 // when vfmt is compiled with `-gc none`:
117 if !foptions.verify_file(prefs, fpath) {
118 println("${file} is not vfmt'ed")
119 errors++
120 }
121 continue
122 }
123 mut worker_command_array := cli_args_no_files.clone()
124 worker_command_array << ['-worker', util.quote_path(fpath)]
125 worker_cmd := worker_command_array.join(' ')
126 foptions.vlog('vfmt worker_cmd: ${worker_cmd}')
127 worker_result := os.execute(worker_cmd)
128 // Guard against a possibly crashing worker process.
129 if worker_result.exit_code != 0 {
130 eprintln(worker_result.output)
131 if worker_result.exit_code == 1 {
132 eprintln('Internal vfmt error while formatting file: ${file}.')
133 has_internal_error = true
134 continue
135 }
136 errors++
137 continue
138 }
139 if worker_result.output.len > 0 {
140 if worker_result.output.contains(formatted_file_token) {
141 wresult := worker_result.output.split(formatted_file_token)
142 formatted_warn_errs := wresult[0]
143 formatted_file_path := wresult[1].trim_right('\n\r')
144 foptions.post_process_file(fpath, formatted_file_path) or { errors = errors + 1 }
145 if formatted_warn_errs.len > 0 {
146 eprintln(formatted_warn_errs)
147 }
148 continue
149 }
150 }
151 errors++
152 }
153 if has_internal_error {
154 // When some files could not be processed due to internal vfmt errors,
155 // exit with code 5 regardless of format-diff errors in other files.
156 // This prevents exit codes like 7 (2+5) that confuse downstream CI checks.
157 exit(5)
158 }
159 if errors > 0 {
160 if !foptions.is_diff {
161 eprintln('Encountered a total of: ${errors} formatting errors.')
162 }
163 match true {
164 foptions.is_noerror { exit(0) }
165 foptions.is_verify { exit(1) }
166 foptions.is_c { exit(2) }
167 else { exit(1) }
168 }
169 }
170 exit(0)
171}
172
173fn (foptions &FormatOptions) verify_file(prefs &pref.Preferences, fpath string) bool {
174 fcontent := foptions.formated_content_from_file(prefs, fpath) or { return false }
175 content := os.read_file(fpath) or { return false }
176 return fcontent == content
177}
178
179fn setup_preferences(args []string) &pref.Preferences {
180 mut prefs, _ := pref.parse_args_and_show_errors(['fmt'], vfmt_args_for_preferences(args), false)
181 prefs.is_fmt = true
182 prefs.skip_warnings = true
183 return prefs
184}
185
186fn vfmt_args_for_preferences(args []string) []string {
187 mut res := []string{}
188 for i := 1; i < args.len; i++ {
189 arg := args[i]
190 if arg == '-worker' {
191 i++
192 continue
193 }
194 if arg in vfmt_only_flags {
195 continue
196 }
197 res << arg
198 }
199 return res
200}
201
202fn setup_preferences_and_table(args []string) (&pref.Preferences, &ast.Table) {
203 return setup_preferences(args), ast.new_table()
204}
205
206fn (foptions &FormatOptions) vlog(msg string) {
207 if foptions.is_verbose {
208 eprintln(msg)
209 }
210}
211
212fn (foptions &FormatOptions) formated_content_from_file(prefs &pref.Preferences, file string) !string {
213 mut table := ast.new_table()
214 file_ast := parser.parse_file(file, mut table, .parse_comments, prefs)
215 if file_ast.errors.len > 0 {
216 return error('the file contains parser errors')
217 }
218 table.new_int = foptions.is_new_int
219 formated_content := fmt.fmt(file_ast, mut table, prefs, foptions.is_debug)
220 return formated_content
221}
222
223fn (foptions &FormatOptions) format_file(file string) {
224 file_name := os.file_name(file)
225 ulid := rand.ulid()
226 vfmt_output_path := os.join_path(vtmp_folder, 'vfmt_${ulid}_${file_name}')
227 if file.contains('_vfmt_off') {
228 os.cp(file, vfmt_output_path) or { panic(err) }
229 foptions.vlog('format_file copied the file ${file} as it was, 1:1, since its name contains `_vfmt_off`.')
230 eprintln('${formatted_file_token}${vfmt_output_path}')
231 return
232 }
233 foptions.vlog('vfmt2 running fmt.fmt over file: ${file}')
234 args := util.join_env_vflags_and_os_args()
235 prefs, mut table := setup_preferences_and_table(args)
236 file_ast := parser.parse_file(file, mut table, .parse_comments, prefs)
237 if file_ast.errors.len > 0 {
238 exit(2)
239 }
240 // checker.new_checker(table, prefs).check(file_ast)
241 table.new_int = foptions.is_new_int
242 formatted_content := fmt.fmt(file_ast, mut table, prefs, foptions.is_debug)
243 os.write_file(vfmt_output_path, formatted_content) or { panic(err) }
244 foptions.vlog('fmt.fmt worked and ${formatted_content.len} bytes were written to ${vfmt_output_path} .')
245 eprintln('${formatted_file_token}${vfmt_output_path}')
246}
247
248fn (foptions &FormatOptions) format_pipe() {
249 foptions.vlog('vfmt2 running fmt.fmt over stdin')
250 args := util.join_env_vflags_and_os_args()
251 prefs, mut table := setup_preferences_and_table(args)
252 input_text := os.get_raw_lines_joined()
253 file_ast := parser.parse_text(input_text, '', mut table, .parse_comments, prefs)
254 if file_ast.errors.len > 0 {
255 exit(1)
256 }
257 // checker.new_checker(table, prefs).check(file_ast)
258 table.new_int = foptions.is_new_int
259 formatted_content := fmt.fmt(file_ast, mut table, prefs, foptions.is_debug,
260 source_text: input_text
261 )
262 print(formatted_content)
263 flush_stdout()
264 foptions.vlog('fmt.fmt worked and ${formatted_content.len} bytes were written to stdout.')
265}
266
267fn (mut foptions FormatOptions) post_process_file(file string, formatted_file_path string) ! {
268 if formatted_file_path == '' {
269 return
270 }
271 fc := os.read_file(file) or {
272 eprintln('File ${file} could not be read')
273 return
274 }
275 formatted_fc := os.read_file(formatted_file_path) or {
276 eprintln('File ${formatted_file_path} could not be read')
277 return
278 }
279 is_formatted_different := fc != formatted_fc
280 if foptions.is_diff {
281 if !is_formatted_different {
282 return
283 }
284 println(diff.compare_files(file, formatted_file_path)!)
285 return error('')
286 }
287 if foptions.is_verify {
288 if !is_formatted_different {
289 return
290 }
291 println("${file} is not vfmt'ed")
292 return error('')
293 }
294 if foptions.is_c {
295 if is_formatted_different {
296 eprintln('File is not formatted: ${file}')
297 return error('')
298 }
299 return
300 }
301 if foptions.is_l {
302 if is_formatted_different {
303 eprintln('File needs formatting: ${file}')
304 }
305 return
306 }
307 if foptions.is_w {
308 if is_formatted_different {
309 if foptions.is_backup {
310 file_bak := '${file}.bak'
311 os.cp(file, file_bak) or {}
312 }
313 mut perms_to_restore := u32(0)
314 $if !windows {
315 fm := os.inode(file)
316 perms_to_restore = fm.bitmask()
317 }
318 os.mv_by_cp(formatted_file_path, file) or { panic(err) }
319 $if !windows {
320 os.chmod(file, int(perms_to_restore)) or { panic(err) }
321 }
322 eprintln('Reformatted file: ${file}')
323 } else {
324 eprintln('Already formatted file: ${file}')
325 }
326 return
327 }
328 print(formatted_fc)
329 flush_stdout()
330}
331
332@[noreturn]
333fn verror(s string) {
334 util.verror('vfmt error', s)
335}
336
337fn (f FormatOptions) str() string {
338 return
339 'FormatOptions{ is_l: ${f.is_l}, is_w: ${f.is_w}, is_diff: ${f.is_diff}, is_verbose: ${f.is_verbose},' +
340 ' is_worker: ${f.is_worker}, is_debug: ${f.is_debug}, is_noerror: ${f.is_noerror},' +
341 ' is_verify: ${f.is_verify}" }'
342}
343