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