v / cmd / tools
Raw file | 292 loc (273 sloc) | 8.07 KB | Latest commit hash 017ace6ea
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 (
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
14struct UndocumentedFN {
15 file string
16 line int
17 signature string
18 tags []string
19}
20
21struct 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
30mut:
31 verify bool
32 diff bool
33 additional_args []string
34}
35
36fn (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
60fn (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
94fn (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
104fn (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
110fn (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
146fn (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
175fn 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
194fn normalise_path(path string) string {
195 return path.replace('\\', '/')
196}
197
198fn 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
214fn collect_tags(line string) []string {
215 mut cleaned := line.all_before('/')
216 cleaned = cleaned.replace_each(['[', '', ']', '', ' ', ''])
217 return cleaned.split(',')
218}
219
220fn 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}