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