1// Copyright (c) 2019-2020 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 v.ast
9import v.pref
10import v.fmt
11import v.util
12import v.parser
13import v.table
14import vhelp
15
16struct FormatOptions {
17	is_l       bool
18	is_c       bool // NB: This refers to the '-c' fmt flag, NOT the C backend
19	is_w       bool
20	is_diff    bool
21	is_verbose bool
22	is_all     bool
23	is_worker  bool
24	is_debug   bool
25	is_noerror bool
26	is_verify  bool // exit(1) if the file is not vfmt'ed
27}
28
29const (
30	formatted_file_token         = '\@\@\@' + 'FORMATTED_FILE: '
31	platform_and_file_extensions = [
32		['windows', '_windows.v'],
33		['linux', '_lin.v', '_linux.v', '_nix.v'],
34		['macos', '_mac.v', '_darwin.v'],
35		['freebsd', '_bsd.v', '_freebsd.v'],
36		['netbsd', '_bsd.v', '_netbsd.v'],
37		['openbsd', '_bsd.v', '_openbsd.v'],
38		['solaris', '_solaris.v'],
39		['haiku', '_haiku.v'],
40		['qnx', '_qnx.v']
41	]
42)
43
44fn main() {
45	// if os.getenv('VFMT_ENABLE') == '' {
46	// eprintln('v fmt is disabled for now')
47	// exit(1)
48	// }
49	toolexe := os.executable()
50	util.set_vroot_folder(os.dir(os.dir(os.dir(toolexe))))
51	args := util.join_env_vflags_and_os_args()
52	foptions := FormatOptions{
53		is_c: '-c' in args
54		is_l: '-l' in args
55		is_w: '-w' in args
56		is_diff: '-diff' in args
57		is_verbose: '-verbose' in args || '--verbose' in args
58		is_all: '-all' in args || '--all' in args
59		is_worker: '-worker' in args
60		is_debug: '-debug' in args
61		is_noerror: '-noerror' in args
62		is_verify: '-verify' in args
63	}
64	if foptions.is_verbose {
65		eprintln('vfmt foptions: $foptions')
66	}
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	mut files := []string{}
85	for file in possible_files {
86		if !file.ends_with('.v') && !file.ends_with('.vv') {
87			verror('v fmt can only be used on .v files.\nOffending file: "$file"')
88			continue
89		}
90		if !os.exists(file) {
91			verror('"$file" does not exist')
92			continue
93		}
94		files << file
95	}
96	if files.len == 0 {
97		vhelp.show_topic('fmt')
98		exit(0)
99	}
100	mut cli_args_no_files := []string{}
101	for a in os.args {
102		if a !in files {
103			cli_args_no_files << a
104		}
105	}
106	mut errors := 0
107	for file in files {
108		fpath := os.real_path(file)
109		mut worker_command_array := cli_args_no_files.clone()
110		worker_command_array << ['-worker', util.quote_path_with_spaces(fpath)]
111		worker_cmd := worker_command_array.join(' ')
112		if foptions.is_verbose {
113			eprintln('vfmt worker_cmd: $worker_cmd')
114		}
115		worker_result := os.exec(worker_cmd) or {
116			errors++
117			continue
118		}
119		if worker_result.exit_code != 0 {
120			eprintln(worker_result.output)
121			if worker_result.exit_code == 1 {
122				eprintln('vfmt error while formatting file: $file .')
123			}
124			errors++
125			continue
126		}
127		if worker_result.output.len > 0 {
128			if worker_result.output.contains(formatted_file_token) {
129				wresult := worker_result.output.split(formatted_file_token)
130				formatted_warn_errs := wresult[0]
131				formatted_file_path := wresult[1].trim_right('\n\r')
132				foptions.post_process_file(fpath, formatted_file_path)
133				if formatted_warn_errs.len > 0 {
134					eprintln(formatted_warn_errs)
135				}
136				continue
137			}
138		}
139		errors++
140	}
141	if errors > 0 {
142		eprintln('Encountered a total of: $errors errors.')
143		if foptions.is_noerror {
144			exit(0)
145		}
146		exit(1)
147	}
148}
149
150fn (foptions &FormatOptions) format_file(file string) {
151	mut prefs := pref.new_preferences()
152	prefs.is_fmt = true
153	if foptions.is_verbose {
154		eprintln('vfmt2 running fmt.fmt over file: $file')
155	}
156	table := table.new_table()
157	// checker := checker.new_checker(table, prefs)
158	file_ast := parser.parse_file(file, table, .parse_comments, prefs, &ast.Scope{
159		parent: 0
160	})
161	// checker.check(file_ast)
162	formatted_content := fmt.fmt(file_ast, table, foptions.is_debug)
163	file_name := os.file_name(file)
164	vfmt_output_path := os.join_path(os.temp_dir(), 'vfmt_' + file_name)
165	os.write_file(vfmt_output_path, formatted_content)
166	if foptions.is_verbose {
167		eprintln('fmt.fmt worked and $formatted_content.len bytes were written to $vfmt_output_path .')
168	}
169	eprintln('$formatted_file_token$vfmt_output_path')
170}
171
172fn print_compiler_options(compiler_params &pref.Preferences) {
173	eprintln('         os: ' + compiler_params.os.str())
174	eprintln('  ccompiler: $compiler_params.ccompiler')
175	eprintln('       path: $compiler_params.path ')
176	eprintln('   out_name: $compiler_params.out_name ')
177	eprintln('      vroot: $compiler_params.vroot ')
178	eprintln('lookup_path: $compiler_params.lookup_path ')
179	eprintln('   out_name: $compiler_params.out_name ')
180	eprintln('     cflags: $compiler_params.cflags ')
181	eprintln('    is_test: $compiler_params.is_test ')
182	eprintln('  is_script: $compiler_params.is_script ')
183}
184
185fn (foptions &FormatOptions) post_process_file(file, formatted_file_path string) {
186	if formatted_file_path.len == 0 {
187		return
188	}
189	if foptions.is_diff {
190		diff_cmd := util.find_working_diff_command() or {
191			eprintln(err)
192			return
193		}
194		if foptions.is_verbose {
195			eprintln('Using diff command: $diff_cmd')
196		}
197		println(util.color_compare_files(diff_cmd, file, formatted_file_path))
198		return
199	}
200	if foptions.is_verify {
201		diff_cmd := util.find_working_diff_command() or {
202			eprintln(err)
203			return
204		}
205		x := util.color_compare_files(diff_cmd, file, formatted_file_path)
206		if x.len != 0 {
207			println("$file is not vfmt'ed")
208			exit(1)
209		}
210		return
211	}
212	fc := os.read_file(file) or {
213		eprintln('File $file could not be read')
214		return
215	}
216	formatted_fc := os.read_file(formatted_file_path) or {
217		eprintln('File $formatted_file_path could not be read')
218		return
219	}
220	is_formatted_different := fc != formatted_fc
221	if foptions.is_c {
222		if is_formatted_different {
223			eprintln('File is not formatted: $file')
224			exit(2)
225		}
226		return
227	}
228	if foptions.is_l {
229		if is_formatted_different {
230			eprintln('File needs formatting: $file')
231		}
232		return
233	}
234	if foptions.is_w {
235		if is_formatted_different {
236			os.mv_by_cp(formatted_file_path, file) or {
237				panic(err)
238			}
239			eprintln('Reformatted file: $file')
240		} else {
241			eprintln('Already formatted file: $file')
242		}
243		return
244	}
245	print(formatted_fc)
246}
247
248fn (f FormatOptions) str() string {
249	return 'FormatOptions{ is_l: $f.is_l, is_w: $f.is_w, is_diff: $f.is_diff, is_verbose: $f.is_verbose,' +
250		' is_all: $f.is_all, is_worker: $f.is_worker, is_debug: $f.is_debug, is_noerror: $f.is_noerror,' +
251		' is_verify: $f.is_verify" }'
252}
253
254fn file_to_target_os(file string) string {
255	for extensions in platform_and_file_extensions {
256		for ext in extensions {
257			if file.ends_with(ext) {
258				return extensions[0]
259			}
260		}
261	}
262	return ''
263}
264
265fn file_to_mod_name_and_is_module_file(file string) (string, bool) {
266	mut mod_name := 'main'
267	mut is_module_file := false
268	flines := read_source_lines(file) or {
269		return mod_name, is_module_file
270	}
271	for fline in flines {
272		line := fline.trim_space()
273		if line.starts_with('module ') {
274			if !line.starts_with('module main') {
275				is_module_file = true
276				mod_name = line.replace('module ', ' ').trim_space()
277			}
278			break
279		}
280	}
281	return mod_name, is_module_file
282}
283
284fn read_source_lines(file string) ?[]string {
285	source_lines := os.read_lines(file) or {
286		return error('can not read $file')
287	}
288	return source_lines
289}
290
291fn get_compile_name_of_potential_v_project(file string) string {
292	// This function get_compile_name_of_potential_v_project returns:
293	// a) the file's folder, if file is part of a v project
294	// b) the file itself, if the file is a standalone v program
295	pfolder := os.real_path(os.dir(file))
296	// a .v project has many 'module main' files in one folder
297	// if there is only one .v file, then it must be a standalone
298	all_files_in_pfolder := os.ls(pfolder) or {
299		panic(err)
300	}
301	mut vfiles := []string{}
302	for f in all_files_in_pfolder {
303		vf := os.join_path(pfolder, f)
304		if f.starts_with('.') || !f.ends_with('.v') || os.is_dir(vf) {
305			continue
306		}
307		vfiles << vf
308	}
309	if vfiles.len == 1 {
310		return file
311	}
312	// /////////////////////////////////////////////////////////////
313	// At this point, we know there are many .v files in the folder
314	// We will have to read them all, and if there are more than one
315	// containing `fn main` then the folder contains multiple standalone
316	// v programs. If only one contains `fn main` then the folder is
317	// a project folder, that should be compiled with `v pfolder`.
318	mut main_fns := 0
319	for f in vfiles {
320		slines := read_source_lines(f) or {
321			panic(err)
322		}
323		for line in slines {
324			if line.contains('fn main()') {
325				main_fns++
326				if main_fns > 1 {
327					return file
328				}
329			}
330		}
331	}
332	return pfolder
333}
334
335fn verror(s string) {
336	util.verror('vfmt error', s)
337}
338