v / cmd / tools / vmissdoc.v
301 lines · 285 sloc · 8.54 KB · e2e5cf8db56f3562c7baa735061690be936bdf3e
Raw
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.
4import os
5import flag
6
7const tool_name = 'v missdoc'
8const tool_version = '0.1.0'
9const 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.'
10const work_dir_prefix = normalise_path(os.real_path(os.wd_at_startup) + os.path_separator)
11
12struct UndocumentedFN {
13 file string
14 line int
15 signature string
16 tags []string
17}
18
19struct 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
28mut:
29 verify bool
30 diff bool
31 additional_args []string
32}
33
34fn (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
58fn (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
93fn (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
103fn (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
108fn (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
148fn (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
179fn 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
198fn normalise_path(path string) string {
199 return path.replace('\\', '/')
200}
201
202fn 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
218fn collect_tags(line string) []string {
219 mut cleaned := line.all_before('/')
220 cleaned = cleaned.replace_each(['@[', '', '[', '', ']', '', ' ', ''])
221 return cleaned.split(',')
222}
223
224fn 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