257 lines
6.4 KiB
V
257 lines
6.4 KiB
V
// Copyright (c) 2019-2022 Alexander Medvednikov. All rights reserved.
|
|
// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
|
|
module main
|
|
|
|
import os
|
|
import os.cmdline
|
|
import v.vet
|
|
import v.pref
|
|
import v.parser
|
|
import v.token
|
|
import v.ast
|
|
import term
|
|
|
|
struct Vet {
|
|
opt Options
|
|
mut:
|
|
errors []vet.Error
|
|
warns []vet.Error
|
|
file string
|
|
}
|
|
|
|
struct Options {
|
|
is_force bool
|
|
is_werror bool
|
|
is_verbose bool
|
|
show_warnings bool
|
|
use_color bool
|
|
}
|
|
|
|
const term_colors = term.can_show_color_on_stderr()
|
|
|
|
fn main() {
|
|
vet_options := cmdline.options_after(os.args, ['vet'])
|
|
mut vt := Vet{
|
|
opt: Options{
|
|
is_force: '-force' in vet_options
|
|
is_werror: '-W' in vet_options
|
|
is_verbose: '-verbose' in vet_options || '-v' in vet_options
|
|
show_warnings: '-hide-warnings' !in vet_options && '-w' !in vet_options
|
|
use_color: '-color' in vet_options || (term_colors && '-nocolor' !in vet_options)
|
|
}
|
|
}
|
|
mut paths := cmdline.only_non_options(vet_options)
|
|
vtmp := os.getenv('VTMP')
|
|
if vtmp != '' {
|
|
// `v test-cleancode` passes also `-o tmpfolder` as well as all options in VFLAGS
|
|
paths = paths.filter(!it.starts_with(vtmp))
|
|
}
|
|
for path in paths {
|
|
if !os.exists(path) {
|
|
eprintln('File/folder $path does not exist')
|
|
continue
|
|
}
|
|
if os.is_file(path) {
|
|
vt.vet_file(path)
|
|
}
|
|
if os.is_dir(path) {
|
|
vt.vprintln("vetting folder: '$path' ...")
|
|
vfiles := os.walk_ext(path, '.v')
|
|
vvfiles := os.walk_ext(path, '.vv')
|
|
mut files := []string{}
|
|
files << vfiles
|
|
files << vvfiles
|
|
for file in files {
|
|
vt.vet_file(file)
|
|
}
|
|
}
|
|
}
|
|
vfmt_err_count := vt.errors.filter(it.fix == .vfmt).len
|
|
if vt.opt.show_warnings {
|
|
for w in vt.warns {
|
|
eprintln(vt.e2string(w))
|
|
}
|
|
}
|
|
for err in vt.errors {
|
|
eprintln(vt.e2string(err))
|
|
}
|
|
if vfmt_err_count > 0 {
|
|
eprintln('Note: You can run `v fmt -w file.v` to fix these errors automatically')
|
|
}
|
|
if vt.errors.len > 0 {
|
|
exit(1)
|
|
}
|
|
}
|
|
|
|
// vet_file vets the file read from `path`.
|
|
fn (mut vt Vet) vet_file(path string) {
|
|
if path.contains('/tests/') && !vt.opt.is_force {
|
|
// skip all /tests/ files, since usually their content is not
|
|
// important enough to be documented/vetted, and they may even
|
|
// contain intentionally invalid code.
|
|
vt.vprintln("skipping test file: '$path' ...")
|
|
return
|
|
}
|
|
vt.file = path
|
|
mut prefs := pref.new_preferences()
|
|
prefs.is_vet = true
|
|
prefs.is_vsh = path.ends_with('.vsh')
|
|
table := ast.new_table()
|
|
vt.vprintln("vetting file '$path'...")
|
|
_, errors := parser.parse_vet_file(path, table, prefs)
|
|
// Transfer errors from scanner and parser
|
|
vt.errors << errors
|
|
// Scan each line in file for things to improve
|
|
source_lines := os.read_lines(vt.file) or { []string{} }
|
|
for lnumber, line in source_lines {
|
|
vt.vet_line(source_lines, line, lnumber)
|
|
}
|
|
}
|
|
|
|
// vet_line vets the contents of `line` from `vet.file`.
|
|
fn (mut vt Vet) vet_line(lines []string, line string, lnumber int) {
|
|
// Vet public functions
|
|
if line.starts_with('pub fn') || (line.starts_with('fn ') && !(line.starts_with('fn C.')
|
|
|| line.starts_with('fn main'))) {
|
|
// Scan function declarations for missing documentation
|
|
is_pub_fn := line.starts_with('pub fn')
|
|
if lnumber > 0 {
|
|
collect_tags := fn (line string) []string {
|
|
mut cleaned := line.all_before('/')
|
|
cleaned = cleaned.replace_each(['[', '', ']', '', ' ', ''])
|
|
return cleaned.split(',')
|
|
}
|
|
ident_fn_name := fn (line string) string {
|
|
mut fn_idx := line.index(' fn ') or { return '' }
|
|
if line.len < fn_idx + 5 {
|
|
return ''
|
|
}
|
|
mut tokens := line[fn_idx + 4..].split(' ')
|
|
// Skip struct identifier
|
|
if tokens.first().starts_with('(') {
|
|
fn_idx = line.index(')') or { return '' }
|
|
tokens = line[fn_idx..].split(' ')
|
|
if tokens.len > 1 {
|
|
tokens = [tokens[1]]
|
|
}
|
|
}
|
|
if tokens.len > 0 {
|
|
return tokens[0].all_before('(')
|
|
}
|
|
return ''
|
|
}
|
|
mut line_above := lines[lnumber - 1]
|
|
mut tags := []string{}
|
|
if !line_above.starts_with('//') {
|
|
mut grab := true
|
|
for j := lnumber - 1; j >= 0; j-- {
|
|
prev_line := lines[j]
|
|
if prev_line.contains('}') { // We've looked back to the above scope, stop here
|
|
break
|
|
} else if prev_line.starts_with('[') {
|
|
tags << collect_tags(prev_line)
|
|
continue
|
|
} else if prev_line.starts_with('//') { // Single-line comment
|
|
grab = false
|
|
break
|
|
}
|
|
}
|
|
if grab {
|
|
clean_line := line.all_before_last('{').trim(' ')
|
|
if is_pub_fn {
|
|
vt.warn('Function documentation seems to be missing for "$clean_line".',
|
|
lnumber, .doc)
|
|
}
|
|
}
|
|
} else {
|
|
fn_name := ident_fn_name(line)
|
|
mut grab := true
|
|
for j := lnumber - 1; j >= 0; j-- {
|
|
prev_line := lines[j]
|
|
if prev_line.contains('}') { // We've looked back to the above scope, stop here
|
|
break
|
|
} else if prev_line.starts_with('// $fn_name ') {
|
|
grab = false
|
|
break
|
|
} else if prev_line.starts_with('// $fn_name') {
|
|
grab = false
|
|
if is_pub_fn {
|
|
clean_line := line.all_before_last('{').trim(' ')
|
|
vt.warn('The documentation for "$clean_line" seems incomplete.',
|
|
lnumber, .doc)
|
|
}
|
|
break
|
|
} else if prev_line.starts_with('[') {
|
|
tags << collect_tags(prev_line)
|
|
continue
|
|
} else if prev_line.starts_with('//') { // Single-line comment
|
|
continue
|
|
}
|
|
}
|
|
if grab {
|
|
clean_line := line.all_before_last('{').trim(' ')
|
|
if is_pub_fn {
|
|
vt.warn('A function name is missing from the documentation of "$clean_line".',
|
|
lnumber, .doc)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn (vt &Vet) vprintln(s string) {
|
|
if !vt.opt.is_verbose {
|
|
return
|
|
}
|
|
println(s)
|
|
}
|
|
|
|
fn (vt &Vet) e2string(err vet.Error) string {
|
|
mut kind := '$err.kind:'
|
|
mut location := '$err.file_path:$err.pos.line_nr:'
|
|
if vt.opt.use_color {
|
|
kind = match err.kind {
|
|
.warning { term.magenta(kind) }
|
|
.error { term.red(kind) }
|
|
}
|
|
kind = term.bold(kind)
|
|
location = term.bold(location)
|
|
}
|
|
return '$location $kind $err.message'
|
|
}
|
|
|
|
fn (mut vt Vet) error(msg string, line int, fix vet.FixKind) {
|
|
pos := token.Pos{
|
|
line_nr: line + 1
|
|
}
|
|
vt.errors << vet.Error{
|
|
message: msg
|
|
file_path: vt.file
|
|
pos: pos
|
|
kind: .error
|
|
fix: fix
|
|
typ: .default
|
|
}
|
|
}
|
|
|
|
fn (mut vt Vet) warn(msg string, line int, fix vet.FixKind) {
|
|
pos := token.Pos{
|
|
line_nr: line + 1
|
|
}
|
|
mut w := vet.Error{
|
|
message: msg
|
|
file_path: vt.file
|
|
pos: pos
|
|
kind: .warning
|
|
fix: fix
|
|
typ: .default
|
|
}
|
|
if vt.opt.is_werror {
|
|
w.kind = .error
|
|
vt.errors << w
|
|
} else {
|
|
vt.warns << w
|
|
}
|
|
}
|