v / cmd / tools
Raw file | 358 loc (342 sloc) | 10.32 KB | Latest commit hash 2029d1830
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.
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_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`
30mut:
31 diff_cmd string // filled in when -diff or -verify is passed
32}
33
34const (
35 formatted_file_token = '\@\@\@' + 'FORMATTED_FILE: '
36 vtmp_folder = os.vtmp_dir()
37 term_colors = term.can_show_color_on_stderr()
38)
39
40fn 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
150fn 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
158fn (foptions &FormatOptions) vlog(msg string) {
159 if foptions.is_verbose {
160 eprintln(msg)
161 }
162}
163
164fn (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
178fn (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
190fn 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
203fn (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
216fn (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
286fn (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
293fn 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
310fn 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
315fn 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]
356fn verror(s string) {
357 util.verror('vfmt error', s)
358}