438 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			V
		
	
	
			
		
		
	
	
			438 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			V
		
	
	
// Copyright (c) 2019-2020 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
 | 
						|
	os.cmdline
 | 
						|
	compiler
 | 
						|
	v.pref
 | 
						|
	v.fmt
 | 
						|
	v.parser
 | 
						|
	v.table
 | 
						|
	vhelp
 | 
						|
)
 | 
						|
 | 
						|
struct FormatOptions {
 | 
						|
	is_2       bool
 | 
						|
	is_l       bool
 | 
						|
	is_c       bool
 | 
						|
	is_w       bool
 | 
						|
	is_diff    bool
 | 
						|
	is_verbose bool
 | 
						|
	is_all     bool
 | 
						|
	is_worker  bool
 | 
						|
	is_debug   bool
 | 
						|
	is_noerror bool
 | 
						|
}
 | 
						|
 | 
						|
const (
 | 
						|
	platform_and_file_extensions = [['windows', '_win.v', '_windows.v'],
 | 
						|
	['linux', '_lin.v', '_linux.v', '_nix.v'],
 | 
						|
	['macos', '_mac.v', '_darwin.v'],
 | 
						|
	['freebsd', '_bsd.v', '_freebsd.v'],
 | 
						|
	['netbsd', '_bsd.v', '_netbsd.v'],
 | 
						|
	['openbsd', '_bsd.v', '_openbsd.v'],
 | 
						|
	['solaris', '_solaris.v'],
 | 
						|
	['haiku', '_haiku.v'],
 | 
						|
	['qnx', '_qnx.v'],
 | 
						|
	]
 | 
						|
	FORMATTED_FILE_TOKEN = '\@\@\@' + 'FORMATTED_FILE: '
 | 
						|
)
 | 
						|
 | 
						|
fn main() {
 | 
						|
	toolexe := os.executable()
 | 
						|
	compiler.set_vroot_folder(os.dir(os.dir(os.dir(toolexe))))
 | 
						|
	args := join_flags_and_argument()
 | 
						|
	foptions := FormatOptions{
 | 
						|
		is_2: '-2' in args
 | 
						|
		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_all: '-all' in args || '--all' in args
 | 
						|
		is_worker: '-worker' in args
 | 
						|
		is_debug: '-debug' in args
 | 
						|
		is_noerror: '-noerror' in args
 | 
						|
	}
 | 
						|
	if foptions.is_verbose {
 | 
						|
		eprintln('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())
 | 
						|
	}
 | 
						|
	mut files := []string
 | 
						|
	for file in possible_files {
 | 
						|
		if foptions.is_2 {
 | 
						|
			if !file.ends_with('.v') && !file.ends_with('.vv') {
 | 
						|
				compiler.verror('v fmt -2 can only be used on .v or .vv files.\nOffending file: "$file" .')
 | 
						|
				continue
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			if !file.ends_with('.v') {
 | 
						|
				compiler.verror('v fmt can only be used on .v files.\nOffending file: "$file" .')
 | 
						|
				continue
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if !os.exists(file) {
 | 
						|
			compiler.verror('"$file" does not exist.')
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		files << file
 | 
						|
	}
 | 
						|
	if files.len == 0 {
 | 
						|
	    vhelp.show_topic('fmt')
 | 
						|
		exit(0)
 | 
						|
	}
 | 
						|
	mut cli_args_no_files := []string
 | 
						|
	for a in os.args {
 | 
						|
		if !a in files {
 | 
						|
			cli_args_no_files << a
 | 
						|
		}
 | 
						|
	}
 | 
						|
	mut errors := 0
 | 
						|
	for file in files {
 | 
						|
		fpath := os.real_path(file)
 | 
						|
		mut worker_command_array := cli_args_no_files.clone()
 | 
						|
		worker_command_array << ['-worker', fpath]
 | 
						|
		worker_cmd := worker_command_array.join(' ')
 | 
						|
		if foptions.is_verbose {
 | 
						|
			eprintln('vfmt worker_cmd: $worker_cmd')
 | 
						|
		}
 | 
						|
		worker_result := os.exec(worker_cmd) or {
 | 
						|
			errors++
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if worker_result.exit_code != 0 {
 | 
						|
			eprintln(worker_result.output)
 | 
						|
			if worker_result.exit_code == 1 {
 | 
						|
				eprintln('vfmt error while formatting file: $file .')
 | 
						|
			}
 | 
						|
			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]
 | 
						|
				foptions.post_process_file(fpath, formatted_file_path)
 | 
						|
				if formatted_warn_errs.len > 0 {
 | 
						|
					eprintln(formatted_warn_errs)
 | 
						|
				}
 | 
						|
				continue
 | 
						|
			}
 | 
						|
		}
 | 
						|
		errors++
 | 
						|
	}
 | 
						|
	if errors > 0 {
 | 
						|
		eprintln('Encountered a total of: ${errors} errors.')
 | 
						|
		if foptions.is_noerror {
 | 
						|
			exit(0)
 | 
						|
		}
 | 
						|
		exit(1)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
fn (foptions &FormatOptions) format_file(file string) {
 | 
						|
	if foptions.is_2 {
 | 
						|
		if foptions.is_verbose {
 | 
						|
			eprintln('vfmt2 running fmt.fmt over file: $file')
 | 
						|
		}
 | 
						|
		table := table.new_table()
 | 
						|
		file_ast := parser.parse_file(file, table, .parse_comments, &pref.Preferences{})
 | 
						|
		formatted_content := fmt.fmt(file_ast, table)
 | 
						|
		file_name := os.file_name(file)
 | 
						|
		vfmt_output_path := os.join_path(os.temp_dir(), 'vfmt_' + file_name)
 | 
						|
		os.write_file(vfmt_output_path, formatted_content )
 | 
						|
		if foptions.is_verbose {
 | 
						|
			eprintln('vfmt2 fmt.fmt worked and ${formatted_content.len} bytes were written to ${vfmt_output_path} .')
 | 
						|
		}
 | 
						|
		eprintln('${FORMATTED_FILE_TOKEN}${vfmt_output_path}')
 | 
						|
		return
 | 
						|
	}
 | 
						|
	tmpfolder := os.temp_dir()
 | 
						|
	mut compiler_params := &pref.Preferences{}
 | 
						|
	target_os := file_to_target_os(file)
 | 
						|
	if target_os != '' {
 | 
						|
		//TODO Remove temporary variable once it compiles correctly in C
 | 
						|
		tmp := pref.os_from_string(target_os) or {
 | 
						|
			eprintln('unknown operating system $target_os')
 | 
						|
			return
 | 
						|
		}
 | 
						|
		compiler_params.os = tmp
 | 
						|
	}
 | 
						|
	mut cfile := file
 | 
						|
	mut mod_folder_parent := tmpfolder
 | 
						|
	is_test_file := file.ends_with('_test.v')
 | 
						|
	mod_name,is_module_file := file_to_mod_name_and_is_module_file(file)
 | 
						|
	use_tmp_main_program := is_module_file && !is_test_file
 | 
						|
	mod_folder := os.base_dir(file)
 | 
						|
	if use_tmp_main_program {
 | 
						|
		// TODO: remove the need for this
 | 
						|
		// This makes a small program that imports the module,
 | 
						|
		// so that the module files will get processed by the
 | 
						|
		// vfmt implementation.
 | 
						|
		mod_folder_parent = os.base_dir(mod_folder)
 | 
						|
		mut main_program_content := if mod_name == 'builtin' || mod_name == 'main' { 'fn main(){}\n' } else { 'import ${mod_name}\n' + 'fn main(){}\n' }
 | 
						|
		main_program_file := os.join_path(tmpfolder,'vfmt_tmp_${mod_name}_program.v')
 | 
						|
		if os.exists(main_program_file) {
 | 
						|
			os.rm(main_program_file)
 | 
						|
		}
 | 
						|
		os.write_file(main_program_file, main_program_content)
 | 
						|
		cfile = main_program_file
 | 
						|
		compiler_params.lookup_path = [mod_folder_parent, '@vlib', '@vmodule']
 | 
						|
	}
 | 
						|
	if !is_test_file && mod_name == 'main' {
 | 
						|
		// NB: here, file is guaranteed to be a main. We do not know however
 | 
						|
		// whether it is a standalone v program, or is it a part of a bigger
 | 
						|
		// project, like vorum or vid.
 | 
						|
		cfile = get_compile_name_of_potential_v_project(cfile)
 | 
						|
	}
 | 
						|
	compiler_params.path = cfile
 | 
						|
	compiler_params.mod = mod_name
 | 
						|
	compiler_params.is_test = is_test_file
 | 
						|
	compiler_params.is_script = file.ends_with('.v') || file.ends_with('.vsh')
 | 
						|
	if foptions.is_verbose {
 | 
						|
		eprintln('vfmt format_file: file: $file')
 | 
						|
		eprintln('vfmt format_file: cfile: $cfile')
 | 
						|
		eprintln('vfmt format_file: is_test_file: $is_test_file')
 | 
						|
		eprintln('vfmt format_file: is_module_file: $is_module_file')
 | 
						|
		eprintln('vfmt format_file: mod_name: $mod_name')
 | 
						|
		eprintln('vfmt format_file: mod_folder: $mod_folder')
 | 
						|
		eprintln('vfmt format_file: mod_folder_parent: $mod_folder_parent')
 | 
						|
		eprintln('vfmt format_file: use_tmp_main_program: $use_tmp_main_program')
 | 
						|
		eprintln('vfmt format_file: compiler_params: ')
 | 
						|
		print_compiler_options( compiler_params )
 | 
						|
		eprintln('-------------------------------------------')
 | 
						|
	}
 | 
						|
	compiler_params.fill_with_defaults()
 | 
						|
	if foptions.is_verbose {
 | 
						|
		eprintln('vfmt format_file: compiler_params: AFTER fill_with_defaults() ')
 | 
						|
		print_compiler_options( compiler_params )
 | 
						|
	}
 | 
						|
	formatted_file_path := foptions.compile_file(file, compiler_params)
 | 
						|
	if use_tmp_main_program {
 | 
						|
		if !foptions.is_debug {
 | 
						|
			os.rm(cfile)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	eprintln('${FORMATTED_FILE_TOKEN}${formatted_file_path}')
 | 
						|
}
 | 
						|
 | 
						|
fn print_compiler_options( compiler_params &pref.Preferences ) {
 | 
						|
	eprintln('         os: ' + compiler_params.os.str() )
 | 
						|
	eprintln('  ccompiler: $compiler_params.ccompiler' )
 | 
						|
	eprintln('        mod: $compiler_params.mod ')
 | 
						|
	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 (foptions &FormatOptions) post_process_file(file string, formatted_file_path string) {
 | 
						|
	if formatted_file_path.len == 0 {
 | 
						|
		return
 | 
						|
	}
 | 
						|
	if foptions.is_diff {
 | 
						|
		diff_cmd := find_working_diff_command() or {
 | 
						|
			eprintln('No working "diff" CLI command found.')
 | 
						|
			return
 | 
						|
		}
 | 
						|
		os.system('$diff_cmd --minimal  --text   --unified=2 --show-function-line="fn " "$file" "$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_c {
 | 
						|
		if is_formatted_different {
 | 
						|
			eprintln('File is not formatted: $file')
 | 
						|
			exit(2)
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
	if foptions.is_l {
 | 
						|
		if is_formatted_different {
 | 
						|
			eprintln('File needs formatting: $file')
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
	if foptions.is_w {
 | 
						|
		if is_formatted_different {
 | 
						|
			os.mv_by_cp(formatted_file_path, file) or {
 | 
						|
				panic(err)
 | 
						|
			}
 | 
						|
			eprintln('Reformatted file: $file')
 | 
						|
		}
 | 
						|
		else {
 | 
						|
			eprintln('Already formatted file: $file')
 | 
						|
		}
 | 
						|
		return
 | 
						|
	}
 | 
						|
	print(formatted_fc)
 | 
						|
}
 | 
						|
 | 
						|
fn find_working_diff_command() ?string {
 | 
						|
	for diffcmd in ['colordiff', 'diff', 'colordiff.exe', 'diff.exe'] {
 | 
						|
		p := os.exec('$diffcmd --version') or {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if p.exit_code == 0 {
 | 
						|
			return diffcmd
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return error('no working diff command found')
 | 
						|
}
 | 
						|
 | 
						|
fn (foptions &FormatOptions) compile_file(file string, compiler_params &pref.Preferences) string {
 | 
						|
	if foptions.is_verbose {
 | 
						|
		eprintln('> new_v_compiler_with_args            file: $file')
 | 
						|
		eprintln('> new_v_compiler_with_args compiler_params:')
 | 
						|
		print_compiler_options( compiler_params )
 | 
						|
	}
 | 
						|
	mut v := compiler.new_v(compiler_params)
 | 
						|
	v.v_fmt_file = file
 | 
						|
	if foptions.is_all {
 | 
						|
		v.v_fmt_all = true
 | 
						|
	}
 | 
						|
	v.compile()
 | 
						|
	return v.v_fmt_file_result
 | 
						|
}
 | 
						|
 | 
						|
pub fn (f FormatOptions) str() string {
 | 
						|
	return 'FormatOptions{ ' + ' is_2: $f.is_2' + ' is_l: $f.is_l' + ' is_w: $f.is_w' + ' is_diff: $f.is_diff' + ' is_verbose: $f.is_verbose' + ' is_all: $f.is_all' + ' is_worker: $f.is_worker' + ' is_debug: $f.is_debug' + ' }'
 | 
						|
}
 | 
						|
 | 
						|
fn file_to_target_os(file string) string {
 | 
						|
	for extensions in platform_and_file_extensions {
 | 
						|
		for ext in extensions {
 | 
						|
			if file.ends_with(ext) {
 | 
						|
				return extensions[0]
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return ''
 | 
						|
}
 | 
						|
 | 
						|
fn file_to_mod_name_and_is_module_file(file string) (string,bool) {
 | 
						|
	mut mod_name := 'main'
 | 
						|
	mut is_module_file := false
 | 
						|
	flines := read_source_lines(file) or {
 | 
						|
		return mod_name,is_module_file
 | 
						|
	}
 | 
						|
	for fline in flines {
 | 
						|
		line := fline.trim_space()
 | 
						|
		if line.starts_with('module ') {
 | 
						|
			if !line.starts_with('module main') {
 | 
						|
				is_module_file = true
 | 
						|
				mod_name = line.replace('module ', ' ').trim_space()
 | 
						|
			}
 | 
						|
			break
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return mod_name,is_module_file
 | 
						|
}
 | 
						|
 | 
						|
fn read_source_lines(file string) ?[]string {
 | 
						|
	source_lines := os.read_lines(file) or {
 | 
						|
		return error('can not read $file')
 | 
						|
	}
 | 
						|
	return source_lines
 | 
						|
}
 | 
						|
 | 
						|
fn get_compile_name_of_potential_v_project(file string) string {
 | 
						|
	// This function get_compile_name_of_potential_v_project returns:
 | 
						|
	// a) the file's folder, if file is part of a v project
 | 
						|
	// b) the file itself, if the file is a standalone v program
 | 
						|
	pfolder := os.real_path(os.dir(file))
 | 
						|
	// a .v project has many 'module main' files in one folder
 | 
						|
	// if there is only one .v file, then it must be a standalone
 | 
						|
	all_files_in_pfolder := os.ls(pfolder) or {
 | 
						|
		panic(err)
 | 
						|
	}
 | 
						|
	mut vfiles := []string
 | 
						|
	for f in all_files_in_pfolder {
 | 
						|
		vf := os.join_path(pfolder, f)
 | 
						|
		if f.starts_with('.') || !f.ends_with('.v') || os.is_dir(vf) {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		vfiles << vf
 | 
						|
	}
 | 
						|
	if vfiles.len == 1 {
 | 
						|
		return file
 | 
						|
	}
 | 
						|
	// /////////////////////////////////////////////////////////////
 | 
						|
	// At this point, we know there are many .v files in the folder
 | 
						|
	// We will have to read them all, and if there are more than one
 | 
						|
	// containing `fn main` then the folder contains multiple standalone
 | 
						|
	// v programs. If only one contains `fn main` then the folder is
 | 
						|
	// a project folder, that should be compiled with `v pfolder`.
 | 
						|
	mut main_fns := 0
 | 
						|
	for f in vfiles {
 | 
						|
		slines := read_source_lines(f) or {
 | 
						|
			panic(err)
 | 
						|
		}
 | 
						|
		for line in slines {
 | 
						|
			if line.contains('fn main()') {
 | 
						|
				main_fns++
 | 
						|
				if main_fns > 1 {
 | 
						|
					return file
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return pfolder
 | 
						|
}
 | 
						|
 | 
						|
//TODO Move join_flags_and_argument() and non_empty() into `cmd/internal` when v.mod work correctly
 | 
						|
//to prevent code duplication with `cmd/v` (cmd/v/flag.v)
 | 
						|
fn join_flags_and_argument() []string {
 | 
						|
	vosargs := os.getenv('VOSARGS')
 | 
						|
	if vosargs != '' {
 | 
						|
		return non_empty(vosargs.split(' '))
 | 
						|
	}
 | 
						|
 | 
						|
	mut args := []string
 | 
						|
	vflags := os.getenv('VFLAGS')
 | 
						|
	if vflags != '' {
 | 
						|
		args << os.args[0]
 | 
						|
		args << vflags.split(' ')
 | 
						|
		if os.args.len > 1 {
 | 
						|
			args << os.args[1..]
 | 
						|
		}
 | 
						|
		return non_empty(args)
 | 
						|
	}
 | 
						|
 | 
						|
	return non_empty(os.args)
 | 
						|
}
 | 
						|
fn non_empty(arg []string) []string {
 | 
						|
	return arg.filter(it != '')
 | 
						|
}
 |