// Copyright (c) 2020 Lars Pontoppidan. All rights reserved. // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. import os import flag const ( tool_name = 'v missdoc' tool_version = '0.1.0' tool_description = 'Prints all V functions in .v files under PATH/, that do not yet have documentation comments.' work_dir_prefix = normalise_path(os.real_path(os.wd_at_startup) + os.path_separator) ) struct UndocumentedFN { file string line int signature string tags []string } struct Options { show_help bool collect_tags bool deprecated bool private bool js bool no_line_numbers bool exclude []string relative_paths bool mut: verify bool diff bool additional_args []string } fn (opt Options) collect_undocumented_functions_in_dir(directory string) []UndocumentedFN { mut files := []string{} collect(directory, mut files, fn (npath string, mut accumulated_paths []string) { if !npath.ends_with('.v') { return } if npath.ends_with('_test.v') { return } accumulated_paths << npath }) mut undocumented_fns := []UndocumentedFN{} for file in files { if !opt.js && file.ends_with('.js.v') { continue } if opt.exclude.len > 0 && opt.exclude.any(file.contains(it)) { continue } undocumented_fns << opt.collect_undocumented_functions_in_file(file) } return undocumented_fns } fn (opt &Options) collect_undocumented_functions_in_file(nfile string) []UndocumentedFN { file := os.real_path(nfile) contents := os.read_file(file) or { panic(err) } lines := contents.split('\n') mut list := []UndocumentedFN{} mut comments := []string{} mut tags := []string{} for i, line in lines { if line.starts_with('//') { comments << line } else if line.trim_space().starts_with('[') { tags << collect_tags(line) } else if line.starts_with('pub fn') || (opt.private && (line.starts_with('fn ') && !(line.starts_with('fn C.') || line.starts_with('fn main')))) { if comments.len == 0 { clean_line := line.all_before_last(' {') list << UndocumentedFN{ line: i + 1 signature: clean_line tags: tags file: file } } tags = [] comments = [] } else { tags = [] comments = [] } } return list } fn (opt &Options) collect_undocumented_functions_in_path(path string) []UndocumentedFN { mut undocumented_functions := []UndocumentedFN{} if os.is_file(path) { undocumented_functions << opt.collect_undocumented_functions_in_file(path) } else { undocumented_functions << opt.collect_undocumented_functions_in_dir(path) } return undocumented_functions } fn (opt &Options) report_undocumented_functions_in_path(path string) int { mut list := opt.collect_undocumented_functions_in_path(path) opt.report_undocumented_functions(list) return list.len } fn (opt &Options) report_undocumented_functions(list []UndocumentedFN) { if list.len > 0 { for undocumented_fn in list { mut line_numbers := '${undocumented_fn.line}:0:' if opt.no_line_numbers { line_numbers = '' } tags_str := if opt.collect_tags && undocumented_fn.tags.len > 0 { '${undocumented_fn.tags}' } else { '' } file := undocumented_fn.file ofile := if opt.relative_paths { file.replace(work_dir_prefix, '') } else { os.real_path(file) } if opt.deprecated { println('${ofile}:${line_numbers}${undocumented_fn.signature} ${tags_str}') } else { mut has_deprecation_tag := false for tag in undocumented_fn.tags { if tag.starts_with('deprecated') { has_deprecation_tag = true break } } if !has_deprecation_tag { println('${ofile}:${line_numbers}${undocumented_fn.signature} ${tags_str}') } } } } } fn (opt &Options) diff_undocumented_functions_in_paths(path_old string, path_new string) []UndocumentedFN { old := os.real_path(path_old) new := os.real_path(path_new) mut old_undocumented_functions := opt.collect_undocumented_functions_in_path(old) mut new_undocumented_functions := opt.collect_undocumented_functions_in_path(new) mut differs := []UndocumentedFN{} if new_undocumented_functions.len > old_undocumented_functions.len { for new_undoc_fn in new_undocumented_functions { new_relative_file := new_undoc_fn.file.replace(new, '').trim_string_left(os.path_separator) mut found := false for old_undoc_fn in old_undocumented_functions { old_relative_file := old_undoc_fn.file.replace(old, '').trim_string_left(os.path_separator) if new_relative_file == old_relative_file && new_undoc_fn.signature == old_undoc_fn.signature { found = true break } } if !found { differs << new_undoc_fn } } } differs.sort_with_compare(sort_undoc_fns) return differs } fn sort_undoc_fns(a &UndocumentedFN, b &UndocumentedFN) int { if a.file < b.file { return -1 } if a.file > b.file { return 1 } // same file sort by signature else { if a.signature < b.signature { return -1 } if a.signature > b.signature { return 1 } return 0 } } fn normalise_path(path string) string { return path.replace('\\', '/') } fn collect(path string, mut l []string, f fn (string, mut []string)) { if !os.is_dir(path) { return } mut files := os.ls(path) or { return } for file in files { p := normalise_path(os.join_path_single(path, file)) if os.is_dir(p) && !os.is_link(p) { collect(p, mut l, f) } else if os.exists(p) { f(p, mut l) } } return } fn collect_tags(line string) []string { mut cleaned := line.all_before('/') cleaned = cleaned.replace_each(['[', '', ']', '', ' ', '']) return cleaned.split(',') } fn main() { mut fp := flag.new_flag_parser(os.args[1..]) // skip the "v" command. fp.application(tool_name) fp.version(tool_version) fp.description(tool_description) fp.arguments_description('PATH [PATH]...') fp.skip_executable() // skip the "missdoc" command. // Collect tool options mut opt := Options{ show_help: fp.bool('help', `h`, false, 'Show this help text.') deprecated: fp.bool('deprecated', `d`, false, 'Include deprecated functions in output.') private: fp.bool('private', `p`, false, 'Include private functions in output.') js: fp.bool('js', 0, false, 'Include JavaScript functions in output.') no_line_numbers: fp.bool('no-line-numbers', `n`, false, 'Exclude line numbers in output.') collect_tags: fp.bool('tags', `t`, false, 'Also print function tags if any is found.') exclude: fp.string_multi('exclude', `e`, '') relative_paths: fp.bool('relative-paths', `r`, false, 'Use relative paths in output.') diff: fp.bool('diff', 0, false, 'exit(1) and show difference between two PATH inputs, return 0 otherwise.') verify: fp.bool('verify', 0, false, 'exit(1) if documentation is missing, 0 otherwise.') } opt.additional_args = fp.finalize() or { panic(err) } if opt.show_help { println(fp.usage()) exit(0) } if opt.additional_args.len == 0 { println(fp.usage()) eprintln('Error: ${tool_name} is missing PATH input') exit(1) } // Allow short-long versions to prevent false positive situations, should // the user miss a `-`. E.g.: the `-verify` flag would be ignored and missdoc // will return 0 for success plus a list of any undocumented functions. if '-verify' in opt.additional_args { opt.verify = true } if '-diff' in opt.additional_args { opt.diff = true } if opt.diff { if opt.additional_args.len < 2 { println(fp.usage()) eprintln('Error: ${tool_name} --diff needs two valid PATH inputs') exit(1) } path_old := opt.additional_args[0] path_new := opt.additional_args[1] if !(os.is_file(path_old) || os.is_dir(path_old)) || !(os.is_file(path_new) || os.is_dir(path_new)) { println(fp.usage()) eprintln('Error: ${tool_name} --diff needs two valid PATH inputs') exit(1) } list := opt.diff_undocumented_functions_in_paths(path_old, path_new) if list.len > 0 { opt.report_undocumented_functions(list) exit(1) } exit(0) } mut total := 0 for path in opt.additional_args { if os.is_file(path) || os.is_dir(path) { total += opt.report_undocumented_functions_in_path(path) } } if opt.verify && total > 0 { exit(1) } }