| 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 | |
| 39 | fn main() { |
| 40 | // if os.getenv('VFMT_ENABLE') == '' { |
| 41 | // eprintln('v fmt is disabled for now') |
| 42 | // exit(1) |
| 43 | // } |
| 44 | toolexe := os.executable() |
| 45 | util.set_vroot_folder(os.dir(os.dir(os.dir(toolexe)))) |
| 46 | args := util.join_env_vflags_and_os_args() |
| 47 | mut foptions := FormatOptions{ |
| 48 | is_c: '-c' in args |
| 49 | is_l: '-l' in args |
| 50 | is_w: '-w' in args |
| 51 | is_diff: '-diff' in args |
| 52 | is_verbose: '-verbose' in args || '--verbose' in args |
| 53 | is_worker: '-worker' in args |
| 54 | is_debug: '-debug' in args |
| 55 | is_noerror: '-noerror' in args |
| 56 | is_verify: '-verify' in args |
| 57 | is_backup: '-backup' in args |
| 58 | in_process: '-inprocess' in args |
| 59 | is_new_int: '-new_int' 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 | } |
| 93 | mut cli_args_no_files := []string{} |
| 94 | for idx, a in os.args { |
| 95 | if idx == 0 { |
| 96 | cli_args_no_files << os.quoted_path(a) |
| 97 | continue |
| 98 | } |
| 99 | if a !in files { |
| 100 | cli_args_no_files << a |
| 101 | } |
| 102 | } |
| 103 | mut errors := 0 |
| 104 | mut has_internal_error := false |
| 105 | mut prefs := setup_preferences() |
| 106 | for file in files { |
| 107 | fpath := os.real_path(file) |
| 108 | if foptions.is_verify && foptions.in_process { |
| 109 | // For a small amount of files, it is faster to process |
| 110 | // everything directly in the same process, single threaded, |
| 111 | // when vfmt is compiled with `-gc none`: |
| 112 | if !foptions.verify_file(prefs, fpath) { |
| 113 | println("${file} is not vfmt'ed") |
| 114 | errors++ |
| 115 | } |
| 116 | continue |
| 117 | } |
| 118 | mut worker_command_array := cli_args_no_files.clone() |
| 119 | worker_command_array << ['-worker', util.quote_path(fpath)] |
| 120 | worker_cmd := worker_command_array.join(' ') |
| 121 | foptions.vlog('vfmt worker_cmd: ${worker_cmd}') |
| 122 | worker_result := os.execute(worker_cmd) |
| 123 | // Guard against a possibly crashing worker process. |
| 124 | if worker_result.exit_code != 0 { |
| 125 | eprintln(worker_result.output) |
| 126 | if worker_result.exit_code == 1 { |
| 127 | eprintln('Internal vfmt error while formatting file: ${file}.') |
| 128 | has_internal_error = true |
| 129 | continue |
| 130 | } |
| 131 | errors++ |
| 132 | continue |
| 133 | } |
| 134 | if worker_result.output.len > 0 { |
| 135 | if worker_result.output.contains(formatted_file_token) { |
| 136 | wresult := worker_result.output.split(formatted_file_token) |
| 137 | formatted_warn_errs := wresult[0] |
| 138 | formatted_file_path := wresult[1].trim_right('\n\r') |
| 139 | foptions.post_process_file(fpath, formatted_file_path) or { errors = errors + 1 } |
| 140 | if formatted_warn_errs.len > 0 { |
| 141 | eprintln(formatted_warn_errs) |
| 142 | } |
| 143 | continue |
| 144 | } |
| 145 | } |
| 146 | errors++ |
| 147 | } |
| 148 | if has_internal_error { |
| 149 | // When some files could not be processed due to internal vfmt errors, |
| 150 | // exit with code 5 regardless of format-diff errors in other files. |
| 151 | // This prevents exit codes like 7 (2+5) that confuse downstream CI checks. |
| 152 | exit(5) |
| 153 | } |
| 154 | if errors > 0 { |
| 155 | if !foptions.is_diff { |
| 156 | eprintln('Encountered a total of: ${errors} formatting errors.') |
| 157 | } |
| 158 | match true { |
| 159 | foptions.is_noerror { exit(0) } |
| 160 | foptions.is_verify { exit(1) } |
| 161 | foptions.is_c { exit(2) } |
| 162 | else { exit(1) } |
| 163 | } |
| 164 | } |
| 165 | exit(0) |
| 166 | } |
| 167 | |
| 168 | fn (foptions &FormatOptions) verify_file(prefs &pref.Preferences, fpath string) bool { |
| 169 | fcontent := foptions.formated_content_from_file(prefs, fpath) or { return false } |
| 170 | content := os.read_file(fpath) or { return false } |
| 171 | return fcontent == content |
| 172 | } |
| 173 | |
| 174 | fn setup_preferences() &pref.Preferences { |
| 175 | mut prefs := pref.new_preferences() |
| 176 | prefs.is_fmt = true |
| 177 | prefs.skip_warnings = true |
| 178 | return prefs |
| 179 | } |
| 180 | |
| 181 | fn setup_preferences_and_table() (&pref.Preferences, &ast.Table) { |
| 182 | return setup_preferences(), ast.new_table() |
| 183 | } |
| 184 | |
| 185 | fn (foptions &FormatOptions) vlog(msg string) { |
| 186 | if foptions.is_verbose { |
| 187 | eprintln(msg) |
| 188 | } |
| 189 | } |
| 190 | |
| 191 | fn (foptions &FormatOptions) formated_content_from_file(prefs &pref.Preferences, file string) !string { |
| 192 | mut table := ast.new_table() |
| 193 | file_ast := parser.parse_file(file, mut table, .parse_comments, prefs) |
| 194 | if file_ast.errors.len > 0 { |
| 195 | return error('the file contains parser errors') |
| 196 | } |
| 197 | table.new_int = foptions.is_new_int |
| 198 | formated_content := fmt.fmt(file_ast, mut table, prefs, foptions.is_debug) |
| 199 | return formated_content |
| 200 | } |
| 201 | |
| 202 | fn (foptions &FormatOptions) format_file(file string) { |
| 203 | file_name := os.file_name(file) |
| 204 | ulid := rand.ulid() |
| 205 | vfmt_output_path := os.join_path(vtmp_folder, 'vfmt_${ulid}_${file_name}') |
| 206 | if file.contains('_vfmt_off') { |
| 207 | os.cp(file, vfmt_output_path) or { panic(err) } |
| 208 | foptions.vlog('format_file copied the file ${file} as it was, 1:1, since its name contains `_vfmt_off`.') |
| 209 | eprintln('${formatted_file_token}${vfmt_output_path}') |
| 210 | return |
| 211 | } |
| 212 | foptions.vlog('vfmt2 running fmt.fmt over file: ${file}') |
| 213 | prefs, mut table := setup_preferences_and_table() |
| 214 | file_ast := parser.parse_file(file, mut table, .parse_comments, prefs) |
| 215 | if file_ast.errors.len > 0 { |
| 216 | exit(2) |
| 217 | } |
| 218 | // checker.new_checker(table, prefs).check(file_ast) |
| 219 | table.new_int = foptions.is_new_int |
| 220 | formatted_content := fmt.fmt(file_ast, mut table, prefs, foptions.is_debug) |
| 221 | os.write_file(vfmt_output_path, formatted_content) or { panic(err) } |
| 222 | foptions.vlog('fmt.fmt worked and ${formatted_content.len} bytes were written to ${vfmt_output_path} .') |
| 223 | eprintln('${formatted_file_token}${vfmt_output_path}') |
| 224 | } |
| 225 | |
| 226 | fn (foptions &FormatOptions) format_pipe() { |
| 227 | foptions.vlog('vfmt2 running fmt.fmt over stdin') |
| 228 | prefs, mut table := setup_preferences_and_table() |
| 229 | input_text := os.get_raw_lines_joined() |
| 230 | file_ast := parser.parse_text(input_text, '', mut table, .parse_comments, prefs) |
| 231 | if file_ast.errors.len > 0 { |
| 232 | exit(1) |
| 233 | } |
| 234 | // checker.new_checker(table, prefs).check(file_ast) |
| 235 | table.new_int = foptions.is_new_int |
| 236 | formatted_content := fmt.fmt(file_ast, mut table, prefs, foptions.is_debug, |
| 237 | source_text: input_text |
| 238 | ) |
| 239 | print(formatted_content) |
| 240 | flush_stdout() |
| 241 | foptions.vlog('fmt.fmt worked and ${formatted_content.len} bytes were written to stdout.') |
| 242 | } |
| 243 | |
| 244 | fn (mut foptions FormatOptions) post_process_file(file string, formatted_file_path string) ! { |
| 245 | if formatted_file_path == '' { |
| 246 | return |
| 247 | } |
| 248 | fc := os.read_file(file) or { |
| 249 | eprintln('File ${file} could not be read') |
| 250 | return |
| 251 | } |
| 252 | formatted_fc := os.read_file(formatted_file_path) or { |
| 253 | eprintln('File ${formatted_file_path} could not be read') |
| 254 | return |
| 255 | } |
| 256 | is_formatted_different := fc != formatted_fc |
| 257 | if foptions.is_diff { |
| 258 | if !is_formatted_different { |
| 259 | return |
| 260 | } |
| 261 | println(diff.compare_files(file, formatted_file_path)!) |
| 262 | return error('') |
| 263 | } |
| 264 | if foptions.is_verify { |
| 265 | if !is_formatted_different { |
| 266 | return |
| 267 | } |
| 268 | println("${file} is not vfmt'ed") |
| 269 | return error('') |
| 270 | } |
| 271 | if foptions.is_c { |
| 272 | if is_formatted_different { |
| 273 | eprintln('File is not formatted: ${file}') |
| 274 | return error('') |
| 275 | } |
| 276 | return |
| 277 | } |
| 278 | if foptions.is_l { |
| 279 | if is_formatted_different { |
| 280 | eprintln('File needs formatting: ${file}') |
| 281 | } |
| 282 | return |
| 283 | } |
| 284 | if foptions.is_w { |
| 285 | if is_formatted_different { |
| 286 | if foptions.is_backup { |
| 287 | file_bak := '${file}.bak' |
| 288 | os.cp(file, file_bak) or {} |
| 289 | } |
| 290 | mut perms_to_restore := u32(0) |
| 291 | $if !windows { |
| 292 | fm := os.inode(file) |
| 293 | perms_to_restore = fm.bitmask() |
| 294 | } |
| 295 | os.mv_by_cp(formatted_file_path, file) or { panic(err) } |
| 296 | $if !windows { |
| 297 | os.chmod(file, int(perms_to_restore)) or { panic(err) } |
| 298 | } |
| 299 | eprintln('Reformatted file: ${file}') |
| 300 | } else { |
| 301 | eprintln('Already formatted file: ${file}') |
| 302 | } |
| 303 | return |
| 304 | } |
| 305 | print(formatted_fc) |
| 306 | flush_stdout() |
| 307 | } |
| 308 | |
| 309 | @[noreturn] |
| 310 | fn verror(s string) { |
| 311 | util.verror('vfmt error', s) |
| 312 | } |
| 313 | |
| 314 | fn (f FormatOptions) str() string { |
| 315 | return |
| 316 | 'FormatOptions{ is_l: ${f.is_l}, is_w: ${f.is_w}, is_diff: ${f.is_diff}, is_verbose: ${f.is_verbose},' + |
| 317 | ' is_worker: ${f.is_worker}, is_debug: ${f.is_debug}, is_noerror: ${f.is_noerror},' + |
| 318 | ' is_verify: ${f.is_verify}" }' |
| 319 | } |
| 320 | |