// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved. // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. module main import os import os.cmdline import rand import term import v.ast import v.pref import v.fmt import v.util import v.util.diff import v.parser import v.help struct FormatOptions { is_l bool is_c bool // Note: This refers to the '-c' fmt flag, NOT the C backend is_w bool is_diff bool is_verbose bool is_debug bool is_noerror bool is_verify bool // exit(1) if the file is not vfmt'ed is_worker bool // true *only* in the worker processes. Note: workers can crash. is_backup bool // make a `file.v.bak` copy *before* overwriting a `file.v` in place with `-w` in_process bool // do not fork a worker process; potentially faster, but more prone to crashes for invalid files mut: diff_cmd string // filled in when -diff or -verify is passed } const formatted_file_token = '\@\@\@' + 'FORMATTED_FILE: ' const vtmp_folder = os.vtmp_dir() const term_colors = term.can_show_color_on_stderr() fn main() { // if os.getenv('VFMT_ENABLE') == '' { // eprintln('v fmt is disabled for now') // exit(1) // } toolexe := os.executable() util.set_vroot_folder(os.dir(os.dir(os.dir(toolexe)))) args := util.join_env_vflags_and_os_args() mut foptions := FormatOptions{ is_c: '-c' in args is_l: '-l' in args is_w: '-w' in args is_diff: '-diff' in args is_verbose: '-verbose' in args || '--verbose' in args is_worker: '-worker' in args is_debug: '-debug' in args is_noerror: '-noerror' in args is_verify: '-verify' in args is_backup: '-backup' in args in_process: '-inprocess' in args } if term_colors { os.setenv('VCOLORS', 'always', true) } foptions.vlog('vfmt foptions: ${foptions}') if foptions.is_worker { // -worker should be added by a parent vfmt process. // We launch a sub process for each file because // the v compiler can do an early exit if it detects // a syntax error, but we want to process ALL passed // files if possible. foptions.format_file(cmdline.option(args, '-worker', '')) exit(0) } // we are NOT a worker at this stage, i.e. we are a parent vfmt process possible_files := cmdline.only_non_options(cmdline.options_after(args, ['fmt'])) if foptions.is_verbose { eprintln('vfmt toolexe: ${toolexe}') eprintln('vfmt args: ' + os.args.str()) eprintln('vfmt env_vflags_and_os_args: ' + args.str()) eprintln('vfmt possible_files: ' + possible_files.str()) } files := util.find_all_v_files(possible_files) or { verror(err.msg()) return } if os.is_atty(0) == 0 && files.len == 0 { foptions.format_pipe() exit(0) } if files.len == 0 || '-help' in args || '--help' in args { help.print_and_exit('fmt') } mut cli_args_no_files := []string{} for idx, a in os.args { if idx == 0 { cli_args_no_files << os.quoted_path(a) continue } if a !in files { cli_args_no_files << a } } mut errors := 0 mut has_internal_error := false mut prefs := setup_preferences() for file in files { fpath := os.real_path(file) if foptions.is_verify && foptions.in_process { // For a small amount of files, it is faster to process // everything directly in the same process, single threaded, // when vfmt is compiled with `-gc none`: if !foptions.verify_file(prefs, fpath) { println("${file} is not vfmt'ed") errors++ } continue } mut worker_command_array := cli_args_no_files.clone() worker_command_array << ['-worker', util.quote_path(fpath)] worker_cmd := worker_command_array.join(' ') foptions.vlog('vfmt worker_cmd: ${worker_cmd}') worker_result := os.execute(worker_cmd) // Guard against a possibly crashing worker process. if worker_result.exit_code != 0 { eprintln(worker_result.output) if worker_result.exit_code == 1 { eprintln('Internal vfmt error while formatting file: ${file}.') has_internal_error = true continue } errors++ continue } if worker_result.output.len > 0 { if worker_result.output.contains(formatted_file_token) { wresult := worker_result.output.split(formatted_file_token) formatted_warn_errs := wresult[0] formatted_file_path := wresult[1].trim_right('\n\r') foptions.post_process_file(fpath, formatted_file_path) or { errors = errors + 1 } if formatted_warn_errs.len > 0 { eprintln(formatted_warn_errs) } continue } } errors++ } ecode := if has_internal_error { 5 } else { 0 } if errors > 0 { if !foptions.is_diff { eprintln('Encountered a total of: ${errors} formatting errors.') } match true { foptions.is_noerror { exit(0 + ecode) } foptions.is_verify { exit(1 + ecode) } foptions.is_c { exit(2 + ecode) } else { exit(1 + ecode) } } } exit(ecode) } fn (foptions &FormatOptions) verify_file(prefs &pref.Preferences, fpath string) bool { fcontent := foptions.formated_content_from_file(prefs, fpath) content := os.read_file(fpath) or { return false } return fcontent == content } fn setup_preferences() &pref.Preferences { mut prefs := pref.new_preferences() prefs.is_fmt = true prefs.skip_warnings = true return prefs } fn setup_preferences_and_table() (&pref.Preferences, &ast.Table) { return setup_preferences(), ast.new_table() } fn (foptions &FormatOptions) vlog(msg string) { if foptions.is_verbose { eprintln(msg) } } fn (foptions &FormatOptions) formated_content_from_file(prefs &pref.Preferences, file string) string { mut table := ast.new_table() file_ast := parser.parse_file(file, mut table, .parse_comments, prefs) formated_content := fmt.fmt(file_ast, mut table, prefs, foptions.is_debug) return formated_content } fn (foptions &FormatOptions) format_file(file string) { file_name := os.file_name(file) ulid := rand.ulid() vfmt_output_path := os.join_path(vtmp_folder, 'vfmt_${ulid}_${file_name}') if file.contains('_vfmt_off') { os.cp(file, vfmt_output_path) or { panic(err) } foptions.vlog('format_file copied the file ${file} as it was, 1:1, since its name contains `_vfmt_off`.') eprintln('${formatted_file_token}${vfmt_output_path}') return } foptions.vlog('vfmt2 running fmt.fmt over file: ${file}') prefs, mut table := setup_preferences_and_table() file_ast := parser.parse_file(file, mut table, .parse_comments, prefs) // checker.new_checker(table, prefs).check(file_ast) formatted_content := fmt.fmt(file_ast, mut table, prefs, foptions.is_debug) os.write_file(vfmt_output_path, formatted_content) or { panic(err) } foptions.vlog('fmt.fmt worked and ${formatted_content.len} bytes were written to ${vfmt_output_path} .') eprintln('${formatted_file_token}${vfmt_output_path}') } fn (foptions &FormatOptions) format_pipe() { foptions.vlog('vfmt2 running fmt.fmt over stdin') prefs, mut table := setup_preferences_and_table() input_text := os.get_raw_lines_joined() file_ast := parser.parse_text(input_text, '', mut table, .parse_comments, prefs) // checker.new_checker(table, prefs).check(file_ast) formatted_content := fmt.fmt(file_ast, mut table, prefs, foptions.is_debug, source_text: input_text ) print(formatted_content) flush_stdout() foptions.vlog('fmt.fmt worked and ${formatted_content.len} bytes were written to stdout.') } fn print_compiler_options(compiler_params &pref.Preferences) { eprintln(' os: ' + compiler_params.os.str()) eprintln(' ccompiler: ${compiler_params.ccompiler}') eprintln(' path: ${compiler_params.path} ') eprintln(' out_name: ${compiler_params.out_name} ') eprintln(' vroot: ${compiler_params.vroot} ') eprintln('lookup_path: ${compiler_params.lookup_path} ') eprintln(' out_name: ${compiler_params.out_name} ') eprintln(' cflags: ${compiler_params.cflags} ') eprintln(' is_test: ${compiler_params.is_test} ') eprintln(' is_script: ${compiler_params.is_script} ') } fn (mut foptions FormatOptions) post_process_file(file string, formatted_file_path string) ! { if formatted_file_path == '' { return } fc := os.read_file(file) or { eprintln('File ${file} could not be read') return } formatted_fc := os.read_file(formatted_file_path) or { eprintln('File ${formatted_file_path} could not be read') return } is_formatted_different := fc != formatted_fc if foptions.is_diff { if !is_formatted_different { return } println(diff.compare_files(file, formatted_file_path)!) return error('') } if foptions.is_verify { if !is_formatted_different { return } println("${file} is not vfmt'ed") return error('') } if foptions.is_c { if is_formatted_different { eprintln('File is not formatted: ${file}') return error('') } return } if foptions.is_l { if is_formatted_different { eprintln('File needs formatting: ${file}') } return } if foptions.is_w { if is_formatted_different { if foptions.is_backup { file_bak := '${file}.bak' os.cp(file, file_bak) or {} } mut perms_to_restore := u32(0) $if !windows { fm := os.inode(file) perms_to_restore = fm.bitmask() } os.mv_by_cp(formatted_file_path, file) or { panic(err) } $if !windows { os.chmod(file, int(perms_to_restore)) or { panic(err) } } eprintln('Reformatted file: ${file}') } else { eprintln('Already formatted file: ${file}') } return } print(formatted_fc) flush_stdout() } fn read_source_lines(file string) ![]string { source_lines := os.read_lines(file) or { return error('can not read ${file}') } return source_lines } @[noreturn] fn verror(s string) { util.verror('vfmt error', s) } fn (f FormatOptions) str() string { return 'FormatOptions{ is_l: ${f.is_l}, is_w: ${f.is_w}, is_diff: ${f.is_diff}, is_verbose: ${f.is_verbose},' + ' is_worker: ${f.is_worker}, is_debug: ${f.is_debug}, is_noerror: ${f.is_noerror},' + ' is_verify: ${f.is_verify}" }' }