| 1 | // Copyright (c) 2020 Lars Pontoppidan. 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 | import os |
| 5 | import flag |
| 6 | |
| 7 | const tool_name = 'v missdoc' |
| 8 | const tool_version = '0.1.0' |
| 9 | const tool_description = 'Prints all V functions in .v files under PATH/, that do not yet have documentation comments.\nNote: use `v missdoc @vlib`, to find public fns that still lack documentation.' |
| 10 | const work_dir_prefix = normalise_path(os.real_path(os.wd_at_startup) + os.path_separator) |
| 11 | |
| 12 | struct UndocumentedFN { |
| 13 | file string |
| 14 | line int |
| 15 | signature string |
| 16 | tags []string |
| 17 | } |
| 18 | |
| 19 | struct Options { |
| 20 | show_help bool |
| 21 | collect_tags bool |
| 22 | deprecated bool |
| 23 | private bool |
| 24 | js bool |
| 25 | no_line_numbers bool |
| 26 | exclude []string |
| 27 | relative_paths bool |
| 28 | mut: |
| 29 | verify bool |
| 30 | diff bool |
| 31 | additional_args []string |
| 32 | } |
| 33 | |
| 34 | fn (opt Options) collect_undocumented_functions_in_dir(directory string) []UndocumentedFN { |
| 35 | mut files := []string{} |
| 36 | collect(directory, mut files, fn (npath string, mut accumulated_paths []string) { |
| 37 | if !npath.ends_with('.v') { |
| 38 | return |
| 39 | } |
| 40 | if npath.ends_with('_test.v') { |
| 41 | return |
| 42 | } |
| 43 | accumulated_paths << npath |
| 44 | }) |
| 45 | mut undocumented_fns := []UndocumentedFN{} |
| 46 | for file in files { |
| 47 | if !opt.js && file.ends_with('.js.v') { |
| 48 | continue |
| 49 | } |
| 50 | if opt.exclude.len > 0 && opt.exclude.any(file.contains(it)) { |
| 51 | continue |
| 52 | } |
| 53 | undocumented_fns << opt.collect_undocumented_functions_in_file(file) |
| 54 | } |
| 55 | return undocumented_fns |
| 56 | } |
| 57 | |
| 58 | fn (opt &Options) collect_undocumented_functions_in_file(nfile string) []UndocumentedFN { |
| 59 | file := os.real_path(nfile) |
| 60 | contents := os.read_file(file) or { panic(err) } |
| 61 | lines := contents.split('\n') |
| 62 | mut list := []UndocumentedFN{} |
| 63 | mut comments := []string{} |
| 64 | mut tags := []string{} |
| 65 | for i, line in lines { |
| 66 | line_trimmed := line.trim_space() |
| 67 | if line.starts_with('//') { |
| 68 | comments << line |
| 69 | } else if line_trimmed.starts_with('@[') || line_trimmed.starts_with('[') { |
| 70 | tags << collect_tags(line) |
| 71 | } else if line.starts_with('pub fn') |
| 72 | || (opt.private && (line.starts_with('fn ') && !(line.starts_with('fn C.') |
| 73 | || line.starts_with('fn main')))) { |
| 74 | if comments.len == 0 { |
| 75 | clean_line := line.all_before_last(' {') |
| 76 | list << UndocumentedFN{ |
| 77 | line: i + 1 |
| 78 | signature: clean_line |
| 79 | tags: tags |
| 80 | file: file |
| 81 | } |
| 82 | } |
| 83 | tags = [] |
| 84 | comments = [] |
| 85 | } else { |
| 86 | tags = [] |
| 87 | comments = [] |
| 88 | } |
| 89 | } |
| 90 | return list |
| 91 | } |
| 92 | |
| 93 | fn (opt &Options) collect_undocumented_functions_in_path(path string) []UndocumentedFN { |
| 94 | mut undocumented_functions := []UndocumentedFN{} |
| 95 | if os.is_file(path) { |
| 96 | undocumented_functions << opt.collect_undocumented_functions_in_file(path) |
| 97 | } else { |
| 98 | undocumented_functions << opt.collect_undocumented_functions_in_dir(path) |
| 99 | } |
| 100 | return undocumented_functions |
| 101 | } |
| 102 | |
| 103 | fn (opt &Options) report_undocumented_functions_in_path(path string) int { |
| 104 | list := opt.collect_undocumented_functions_in_path(path) |
| 105 | return opt.report_undocumented_functions(list) |
| 106 | } |
| 107 | |
| 108 | fn (opt &Options) report_undocumented_functions(list []UndocumentedFN) int { |
| 109 | mut nreports := 0 |
| 110 | if list.len > 0 { |
| 111 | for undocumented_fn in list { |
| 112 | mut line_numbers := '${undocumented_fn.line}:0:' |
| 113 | if opt.no_line_numbers { |
| 114 | line_numbers = '' |
| 115 | } |
| 116 | tags_str := if opt.collect_tags && undocumented_fn.tags.len > 0 { |
| 117 | '${undocumented_fn.tags}' |
| 118 | } else { |
| 119 | '' |
| 120 | } |
| 121 | file := undocumented_fn.file |
| 122 | ofile := if opt.relative_paths { |
| 123 | file.replace(work_dir_prefix, '') |
| 124 | } else { |
| 125 | os.real_path(file) |
| 126 | } |
| 127 | if opt.deprecated { |
| 128 | println('${ofile}:${line_numbers}${undocumented_fn.signature} ${tags_str}') |
| 129 | nreports++ |
| 130 | } else { |
| 131 | mut has_deprecation_tag := false |
| 132 | for tag in undocumented_fn.tags { |
| 133 | if tag.starts_with('deprecated') { |
| 134 | has_deprecation_tag = true |
| 135 | break |
| 136 | } |
| 137 | } |
| 138 | if !has_deprecation_tag { |
| 139 | println('${ofile}:${line_numbers}${undocumented_fn.signature} ${tags_str}') |
| 140 | nreports++ |
| 141 | } |
| 142 | } |
| 143 | } |
| 144 | } |
| 145 | return nreports |
| 146 | } |
| 147 | |
| 148 | fn (opt &Options) diff_undocumented_functions_in_paths(path_old string, path_new string) []UndocumentedFN { |
| 149 | old := os.real_path(path_old) |
| 150 | new := os.real_path(path_new) |
| 151 | |
| 152 | mut old_undocumented_functions := opt.collect_undocumented_functions_in_path(old) |
| 153 | mut new_undocumented_functions := opt.collect_undocumented_functions_in_path(new) |
| 154 | |
| 155 | mut differs := []UndocumentedFN{} |
| 156 | if new_undocumented_functions.len > old_undocumented_functions.len { |
| 157 | for new_undoc_fn in new_undocumented_functions { |
| 158 | new_relative_file := |
| 159 | new_undoc_fn.file.replace(new, '').trim_string_left(os.path_separator) |
| 160 | mut found := false |
| 161 | for old_undoc_fn in old_undocumented_functions { |
| 162 | old_relative_file := |
| 163 | old_undoc_fn.file.replace(old, '').trim_string_left(os.path_separator) |
| 164 | if new_relative_file == old_relative_file |
| 165 | && new_undoc_fn.signature == old_undoc_fn.signature { |
| 166 | found = true |
| 167 | break |
| 168 | } |
| 169 | } |
| 170 | if !found { |
| 171 | differs << new_undoc_fn |
| 172 | } |
| 173 | } |
| 174 | } |
| 175 | differs.sort_with_compare(sort_undoc_fns) |
| 176 | return differs |
| 177 | } |
| 178 | |
| 179 | fn sort_undoc_fns(a &UndocumentedFN, b &UndocumentedFN) int { |
| 180 | if a.file < b.file { |
| 181 | return -1 |
| 182 | } |
| 183 | if a.file > b.file { |
| 184 | return 1 |
| 185 | } |
| 186 | // same file sort by signature |
| 187 | else { |
| 188 | if a.signature < b.signature { |
| 189 | return -1 |
| 190 | } |
| 191 | if a.signature > b.signature { |
| 192 | return 1 |
| 193 | } |
| 194 | return 0 |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | fn normalise_path(path string) string { |
| 199 | return path.replace('\\', '/') |
| 200 | } |
| 201 | |
| 202 | fn collect(path string, mut l []string, f fn (string, mut []string)) { |
| 203 | if !os.is_dir(path) { |
| 204 | return |
| 205 | } |
| 206 | mut files := os.ls(path) or { return } |
| 207 | for file in files { |
| 208 | p := normalise_path(os.join_path_single(path, file)) |
| 209 | if os.is_dir(p) && !os.is_link(p) { |
| 210 | collect(p, mut l, f) |
| 211 | } else if os.exists(p) { |
| 212 | f(p, mut l) |
| 213 | } |
| 214 | } |
| 215 | return |
| 216 | } |
| 217 | |
| 218 | fn collect_tags(line string) []string { |
| 219 | mut cleaned := line.all_before('/') |
| 220 | cleaned = cleaned.replace_each(['@[', '', '[', '', ']', '', ' ', '']) |
| 221 | return cleaned.split(',') |
| 222 | } |
| 223 | |
| 224 | fn main() { |
| 225 | mut fp := flag.new_flag_parser(os.args[1..]) // skip the "v" command. |
| 226 | fp.application(tool_name) |
| 227 | fp.version(tool_version) |
| 228 | fp.description(tool_description) |
| 229 | fp.arguments_description('PATH [PATH]...') |
| 230 | fp.skip_executable() // skip the "missdoc" command. |
| 231 | // Collect tool options |
| 232 | mut opt := Options{ |
| 233 | show_help: fp.bool('help', `h`, false, 'Show this help text.') |
| 234 | deprecated: fp.bool('deprecated', `d`, false, |
| 235 | 'Include deprecated functions in output.') |
| 236 | private: fp.bool('private', `p`, false, 'Include private functions in output.') |
| 237 | js: fp.bool('js', 0, false, 'Include JavaScript functions in output.') |
| 238 | no_line_numbers: fp.bool('no-line-numbers', `n`, false, 'Exclude line numbers in output.') |
| 239 | collect_tags: fp.bool('tags', `t`, false, 'Also print function tags if any is found.') |
| 240 | exclude: fp.string_multi('exclude', `e`, '') |
| 241 | relative_paths: fp.bool('relative-paths', `r`, false, 'Use relative paths in output.') |
| 242 | diff: fp.bool('diff', 0, false, |
| 243 | 'exit(1) and show difference between two PATH inputs, return 0 otherwise.') |
| 244 | verify: fp.bool('verify', 0, false, |
| 245 | 'exit(1) if documentation is missing, 0 otherwise.') |
| 246 | } |
| 247 | opt.additional_args = fp.finalize() or { panic(err) } |
| 248 | if opt.show_help { |
| 249 | println(fp.usage()) |
| 250 | exit(0) |
| 251 | } |
| 252 | if opt.additional_args.len == 0 { |
| 253 | println(fp.usage()) |
| 254 | eprintln('Error: ${tool_name} is missing PATH input') |
| 255 | exit(1) |
| 256 | } |
| 257 | // Allow short-long versions to prevent false positive situations, should |
| 258 | // the user miss a `-`. E.g.: the `-verify` flag would be ignored and missdoc |
| 259 | // will return 0 for success plus a list of any undocumented functions. |
| 260 | if '-verify' in opt.additional_args { |
| 261 | opt.verify = true |
| 262 | } |
| 263 | if '-diff' in opt.additional_args { |
| 264 | opt.diff = true |
| 265 | } |
| 266 | if opt.diff { |
| 267 | if opt.additional_args.len < 2 { |
| 268 | println(fp.usage()) |
| 269 | eprintln('Error: ${tool_name} --diff needs two valid PATH inputs') |
| 270 | exit(1) |
| 271 | } |
| 272 | path_old := opt.additional_args[0] |
| 273 | path_new := opt.additional_args[1] |
| 274 | if !(os.is_file(path_old) || os.is_dir(path_old)) || !(os.is_file(path_new) |
| 275 | || os.is_dir(path_new)) { |
| 276 | println(fp.usage()) |
| 277 | eprintln('Error: ${tool_name} --diff needs two valid PATH inputs') |
| 278 | exit(1) |
| 279 | } |
| 280 | list := opt.diff_undocumented_functions_in_paths(path_old, path_new) |
| 281 | nreports := opt.report_undocumented_functions(list) |
| 282 | if nreports > 0 { |
| 283 | exit(1) |
| 284 | } |
| 285 | exit(0) |
| 286 | } |
| 287 | mut total := 0 |
| 288 | for path in opt.additional_args { |
| 289 | if path in ['@vlib', '@cmd', '@examples'] { |
| 290 | rpath := path[1..] |
| 291 | total += opt.report_undocumented_functions_in_path(os.join_path(@VEXEROOT, rpath)) |
| 292 | continue |
| 293 | } |
| 294 | if os.is_file(path) || os.is_dir(path) { |
| 295 | total += opt.report_undocumented_functions_in_path(path) |
| 296 | } |
| 297 | } |
| 298 | if opt.verify && total > 0 { |
| 299 | exit(1) |
| 300 | } |
| 301 | } |
| 302 | |