| 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. |
| 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_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. |
| 31 | mut: |
| 32 | diff_cmd string // filled in when -diff or -verify is passed |
| 33 | } |
| 34 | |
| 35 | const formatted_file_token = '\@\@\@' + 'FORMATTED_FILE: ' |
| 36 | const vtmp_folder = os.vtmp_dir() |
| 37 | const term_colors = term.can_show_color_on_stderr() |
| 38 | const vfmt_only_flags = ['-backup', '-c', '-diff', '-inprocess', '-l', '-new_int', '-noerror', |
| 39 | '-verbose', '--verbose', '-verify', '-w'] |
| 40 | |
| 41 | fn 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 | |
| 173 | fn (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 | |
| 179 | fn 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 | |
| 186 | fn 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 | |
| 202 | fn setup_preferences_and_table(args []string) (&pref.Preferences, &ast.Table) { |
| 203 | return setup_preferences(args), ast.new_table() |
| 204 | } |
| 205 | |
| 206 | fn (foptions &FormatOptions) vlog(msg string) { |
| 207 | if foptions.is_verbose { |
| 208 | eprintln(msg) |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | fn (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 | |
| 223 | fn (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 | |
| 248 | fn (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 | |
| 267 | fn (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] |
| 333 | fn verror(s string) { |
| 334 | util.verror('vfmt error', s) |
| 335 | } |
| 336 | |
| 337 | fn (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 | |