tools: implement `v missdoc --diff oldv newv` (#14537)

Larpon 2022-05-27 17:19:06 +02:00 committed by Chewing_Bever
parent 3d18c884d4
commit 31efb48fc5
Signed by: Jef Roosens
GPG Key ID: B75D4F293C7052DB
4 changed files with 163 additions and 39 deletions

View File

@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
timeout-minutes: 5 timeout-minutes: 5
env: env:
MOPTIONS: --no-line-numbers --relative-paths --exclude /vlib/v/ --exclude /builtin/linux_bare/ --exclude /testdata/ --exclude /tests/ vlib/ MOPTIONS: --relative-paths --exclude /vlib/v/ --exclude /builtin/linux_bare/ --exclude /testdata/ --exclude /tests/
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Build V - name: Build V
@ -35,14 +35,4 @@ jobs:
- name: Check against parent commit - name: Check against parent commit
run: | run: |
./v missdoc $MOPTIONS | sort > /tmp/n_v.txt ./v missdoc --diff $MOPTIONS pv/vlib vlib
cd pv/ && ../v missdoc $MOPTIONS | sort > /tmp/o_v.txt
count_new=$(cat /tmp/n_v.txt | wc -l)
count_old=$(cat /tmp/o_v.txt | wc -l)
echo "new pubs: $count_new | old pubs: $count_old"
echo "new head: $(head -n1 /tmp/n_v.txt)"
echo "old head: $(head -n1 /tmp/o_v.txt)"
if [[ ${count_new} -gt ${count_old} ]]; then
echo "The following $((count_new-count_old)) function(s) are introduced with no documentation:"
diff /tmp/n_v.txt /tmp/o_v.txt ## diff does exit(1) when files are different
fi

View File

@ -259,6 +259,8 @@ const (
'--relative-paths', '--relative-paths',
'-r', '-r',
'--js', '--js',
'--verify',
'--diff',
] ]
auto_complete_flags_self = [ auto_complete_flags_self = [
'-prod', '-prod',

View File

@ -6,12 +6,13 @@ import flag
const ( const (
tool_name = 'v missdoc' tool_name = 'v missdoc'
tool_version = '0.0.5' tool_version = '0.1.0'
tool_description = 'Prints all V functions in .v files under PATH/, that do not yet have documentation comments.' tool_description = 'Prints all V functions in .v files under PATH/, that do not yet have documentation comments.'
work_dir_prefix = normalise_path(os.real_path(os.wd_at_startup) + '/') work_dir_prefix = normalise_path(os.real_path(os.wd_at_startup) + os.path_separator)
) )
struct UndocumentedFN { struct UndocumentedFN {
file string
line int line int
signature string signature string
tags []string tags []string
@ -26,12 +27,15 @@ struct Options {
no_line_numbers bool no_line_numbers bool
exclude []string exclude []string
relative_paths bool relative_paths bool
mut:
verify bool verify bool
diff bool
additional_args []string
} }
fn (opt Options) report_undocumented_functions_in_path(path string) int { fn (opt Options) collect_undocumented_functions_in_dir(directory string) []UndocumentedFN {
mut files := []string{} mut files := []string{}
collect(path, mut files, fn (npath string, mut accumulated_paths []string) { collect(directory, mut files, fn (npath string, mut accumulated_paths []string) {
if !npath.ends_with('.v') { if !npath.ends_with('.v') {
return return
} }
@ -40,7 +44,7 @@ fn (opt Options) report_undocumented_functions_in_path(path string) int {
} }
accumulated_paths << npath accumulated_paths << npath
}) })
mut undocumented_fn_total := 0 mut undocumented_fns := []UndocumentedFN{}
for file in files { for file in files {
if !opt.js && file.ends_with('.js.v') { if !opt.js && file.ends_with('.js.v') {
continue continue
@ -48,16 +52,16 @@ fn (opt Options) report_undocumented_functions_in_path(path string) int {
if opt.exclude.len > 0 && opt.exclude.any(file.contains(it)) { if opt.exclude.len > 0 && opt.exclude.any(file.contains(it)) {
continue continue
} }
undocumented_fn_total += opt.report_undocumented_functions_in_file(file) undocumented_fns << opt.collect_undocumented_functions_in_file(file)
} }
return undocumented_fn_total return undocumented_fns
} }
fn (opt &Options) report_undocumented_functions_in_file(nfile string) int { fn (opt &Options) collect_undocumented_functions_in_file(nfile string) []UndocumentedFN {
file := os.real_path(nfile) file := os.real_path(nfile)
contents := os.read_file(file) or { panic(err) } contents := os.read_file(file) or { panic(err) }
lines := contents.split('\n') lines := contents.split('\n')
mut info := []UndocumentedFN{} mut list := []UndocumentedFN{}
for i, line in lines { for i, line in lines {
if line.starts_with('pub fn') || (opt.private && (line.starts_with('fn ') if line.starts_with('pub fn') || (opt.private && (line.starts_with('fn ')
&& !(line.starts_with('fn C.') || line.starts_with('fn main')))) { && !(line.starts_with('fn C.') || line.starts_with('fn main')))) {
@ -81,14 +85,39 @@ fn (opt &Options) report_undocumented_functions_in_file(nfile string) int {
} }
if grab { if grab {
clean_line := line.all_before_last(' {') clean_line := line.all_before_last(' {')
info << UndocumentedFN{i + 1, clean_line, tags} list << UndocumentedFN{
line: i + 1
signature: clean_line
tags: tags
file: file
}
} }
} }
} }
} }
} }
if info.len > 0 { return list
for undocumented_fn in info { }
fn (opt &Options) collect_undocumented_functions_in_path(path string) []UndocumentedFN {
mut undocumented_functions := []UndocumentedFN{}
if os.is_file(path) {
undocumented_functions << opt.collect_undocumented_functions_in_file(path)
} else {
undocumented_functions << opt.collect_undocumented_functions_in_dir(path)
}
return undocumented_functions
}
fn (opt &Options) report_undocumented_functions_in_path(path string) int {
mut list := opt.collect_undocumented_functions_in_path(path)
opt.report_undocumented_functions(list)
return list.len
}
fn (opt &Options) report_undocumented_functions(list []UndocumentedFN) {
if list.len > 0 {
for undocumented_fn in list {
mut line_numbers := '$undocumented_fn.line:0:' mut line_numbers := '$undocumented_fn.line:0:'
if opt.no_line_numbers { if opt.no_line_numbers {
line_numbers = '' line_numbers = ''
@ -98,10 +127,11 @@ fn (opt &Options) report_undocumented_functions_in_file(nfile string) int {
} else { } else {
'' ''
} }
file := undocumented_fn.file
ofile := if opt.relative_paths { ofile := if opt.relative_paths {
nfile.replace(work_dir_prefix, '') file.replace(work_dir_prefix, '')
} else { } else {
os.real_path(nfile) os.real_path(file)
} }
if opt.deprecated { if opt.deprecated {
println('$ofile:$line_numbers$undocumented_fn.signature $tags_str') println('$ofile:$line_numbers$undocumented_fn.signature $tags_str')
@ -119,7 +149,54 @@ fn (opt &Options) report_undocumented_functions_in_file(nfile string) int {
} }
} }
} }
return info.len }
fn (opt &Options) diff_undocumented_functions_in_paths(path_old string, path_new string) []UndocumentedFN {
old := os.real_path(path_old)
new := os.real_path(path_new)
mut old_undocumented_functions := opt.collect_undocumented_functions_in_path(old)
mut new_undocumented_functions := opt.collect_undocumented_functions_in_path(new)
mut differs := []UndocumentedFN{}
if new_undocumented_functions.len > old_undocumented_functions.len {
for new_undoc_fn in new_undocumented_functions {
new_relative_file := new_undoc_fn.file.replace(new, '').trim_string_left(os.path_separator)
mut found := false
for old_undoc_fn in old_undocumented_functions {
old_relative_file := old_undoc_fn.file.replace(old, '').trim_string_left(os.path_separator)
if new_relative_file == old_relative_file
&& new_undoc_fn.signature == old_undoc_fn.signature {
found = true
break
}
}
if !found {
differs << new_undoc_fn
}
}
}
differs.sort_with_compare(sort_undoc_fns)
return differs
}
fn sort_undoc_fns(a &UndocumentedFN, b &UndocumentedFN) int {
if a.file < b.file {
return -1
}
if a.file > b.file {
return 1
}
// same file sort by signature
else {
if a.signature < b.signature {
return -1
}
if a.signature > b.signature {
return 1
}
return 0
}
} }
fn normalise_path(path string) string { fn normalise_path(path string) string {
@ -149,17 +226,15 @@ fn collect_tags(line string) []string {
} }
fn main() { fn main() {
if os.args.len == 1 { mut fp := flag.new_flag_parser(os.args[1..]) // skip the "v" command.
println('Usage: $tool_name PATH \n$tool_description\n$tool_name -h for more help...')
exit(1)
}
mut fp := flag.new_flag_parser(os.args[1..])
fp.application(tool_name) fp.application(tool_name)
fp.version(tool_version) fp.version(tool_version)
fp.description(tool_description) fp.description(tool_description)
fp.arguments_description('PATH [PATH]...') fp.arguments_description('PATH [PATH]...')
fp.skip_executable() // skip the "missdoc" command.
// Collect tool options // Collect tool options
opt := Options{ mut opt := Options{
show_help: fp.bool('help', `h`, false, 'Show this help text.') show_help: fp.bool('help', `h`, false, 'Show this help text.')
deprecated: fp.bool('deprecated', `d`, false, 'Include deprecated functions in output.') deprecated: fp.bool('deprecated', `d`, false, 'Include deprecated functions in output.')
private: fp.bool('private', `p`, false, 'Include private functions in output.') private: fp.bool('private', `p`, false, 'Include private functions in output.')
@ -168,17 +243,54 @@ fn main() {
collect_tags: fp.bool('tags', `t`, false, 'Also print function tags if any is found.') collect_tags: fp.bool('tags', `t`, false, 'Also print function tags if any is found.')
exclude: fp.string_multi('exclude', `e`, '') exclude: fp.string_multi('exclude', `e`, '')
relative_paths: fp.bool('relative-paths', `r`, false, 'Use relative paths in output.') relative_paths: fp.bool('relative-paths', `r`, false, 'Use relative paths in output.')
diff: fp.bool('diff', 0, false, 'exit(1) and show difference between two PATH inputs, return 0 otherwise.')
verify: fp.bool('verify', 0, false, 'exit(1) if documentation is missing, 0 otherwise.') verify: fp.bool('verify', 0, false, 'exit(1) if documentation is missing, 0 otherwise.')
} }
opt.additional_args = fp.finalize() or { panic(err) }
if opt.show_help { if opt.show_help {
println(fp.usage()) println(fp.usage())
exit(0) exit(0)
} }
if opt.additional_args.len == 0 {
println(fp.usage())
eprintln('Error: $tool_name is missing PATH input')
exit(1)
}
// Allow short-long versions to prevent false positive situations, should
// the user miss a `-`. E.g.: the `-verify` flag would be ignored and missdoc
// will return 0 for success plus a list of any undocumented functions.
if '-verify' in opt.additional_args {
opt.verify = true
}
if '-diff' in opt.additional_args {
opt.diff = true
}
if opt.diff {
if opt.additional_args.len < 2 {
println(fp.usage())
eprintln('Error: $tool_name --diff needs two valid PATH inputs')
exit(1)
}
path_old := opt.additional_args[0]
path_new := opt.additional_args[1]
if !(os.is_file(path_old) || os.is_dir(path_old)) || !(os.is_file(path_new)
|| os.is_dir(path_new)) {
println(fp.usage())
eprintln('Error: $tool_name --diff needs two valid PATH inputs')
exit(1)
}
list := opt.diff_undocumented_functions_in_paths(path_old, path_new)
if list.len > 0 {
opt.report_undocumented_functions(list)
exit(1)
}
exit(0)
}
mut total := 0 mut total := 0
for path in os.args[1..] { for path in opt.additional_args {
if os.is_file(path) { if os.is_file(path) || os.is_dir(path) {
total += opt.report_undocumented_functions_in_file(path)
} else {
total += opt.report_undocumented_functions_in_path(path) total += opt.report_undocumented_functions_in_path(path)
} }
} }

View File

@ -1,4 +1,4 @@
v missdoc 0.0.4 v missdoc 0.1.0
----------------------------------------------- -----------------------------------------------
Usage: v missdoc [options] PATH [PATH]... Usage: v missdoc [options] PATH [PATH]...
@ -14,3 +14,23 @@ Options:
-e, --exclude <multiple strings> -e, --exclude <multiple strings>
-r, --relative-paths Use relative paths in output. -r, --relative-paths Use relative paths in output.
--verify exit(1) if documentation is missing, 0 otherwise.
--diff exit(1) and show difference between two PATH inputs, return 0 otherwise.
--version output version information and exit
-----------------------------------------------
PATH can be both files and directories.
The `--verify` flag is useful for use in CI setups for checking if a V project
has all it's functions and methods documented:
```
v missdoc --verify path/to/code
```
The `--diff` flag is useful if your project is not yet fully documented
but you want to ensure that no new functions or methods are introduced
between commits or branches:
```
v missdoc --diff current/code new/code
```