Skip to content

Commit f75aa34

Browse files
authored
vvet: fix for v vet folder/ + new features (track long fns, empty fns and repeated code), enabled by the new -F and -r flags (#23405)
1 parent 0ee49c6 commit f75aa34

11 files changed

Lines changed: 351 additions & 34 deletions

File tree

‎cmd/tools/vvet/analyze.v‎

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright (c) 2025 Felipe Pena. All rights reserved.
2+
// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
3+
module main
4+
5+
import v.ast
6+
import v.token
7+
import arrays
8+
9+
// cutoffs
10+
const indexexpr_cutoff = 10
11+
const infixexpr_cutoff = 10
12+
const selectorexpr_cutoff = 10
13+
const callexpr_cutoff = 10
14+
const stringinterliteral_cutoff = 10
15+
const stringliteral_cutoff = 10
16+
const ascast_cutoff = 10
17+
const stringconcat_cutoff = 10
18+
19+
// minimum size for string literals
20+
const stringliteral_min_size = 20
21+
22+
// long functions cutoff
23+
const long_fns_cutoff = 300
24+
25+
struct VetAnalyze {
26+
mut:
27+
repeated_expr_cutoff shared map[string]int // repeated code cutoff
28+
repeated_expr shared map[string]map[string]map[string][]token.Pos // repeated exprs in fn scope
29+
cur_fn ast.FnDecl // current fn declaration
30+
}
31+
32+
// stmt checks for repeated code in statements
33+
fn (mut vt VetAnalyze) stmt(vet &Vet, stmt ast.Stmt) {
34+
match stmt {
35+
ast.AssignStmt {
36+
if stmt.op == .plus_assign {
37+
if stmt.right[0] in [ast.StringLiteral, ast.StringInterLiteral] {
38+
vt.save_expr(stringconcat_cutoff, '${stmt.left[0].str()} += ${stmt.right[0].str()}',
39+
vet.file, stmt.pos)
40+
}
41+
}
42+
}
43+
else {}
44+
}
45+
}
46+
47+
// save_expr registers a repeated code occurrence
48+
fn (mut vt VetAnalyze) save_expr(cutoff int, expr string, file string, pos token.Pos) {
49+
lock vt.repeated_expr {
50+
vt.repeated_expr[vt.cur_fn.name][expr][file] << pos
51+
}
52+
lock vt.repeated_expr_cutoff {
53+
vt.repeated_expr_cutoff[expr] = cutoff
54+
}
55+
}
56+
57+
// exprs checks for repeated code in expressions
58+
fn (mut vt VetAnalyze) exprs(vet &Vet, exprs []ast.Expr) {
59+
for expr in exprs {
60+
vt.expr(vet, expr)
61+
}
62+
}
63+
64+
// expr checks for repeated code
65+
fn (mut vt VetAnalyze) expr(vet &Vet, expr ast.Expr) {
66+
match expr {
67+
ast.InfixExpr {
68+
vt.save_expr(infixexpr_cutoff, '${expr.left} ${expr.op} ${expr.right}', vet.file,
69+
expr.pos)
70+
}
71+
ast.IndexExpr {
72+
vt.save_expr(indexexpr_cutoff, '${expr.left}[${expr.index}]', vet.file, expr.pos)
73+
}
74+
ast.SelectorExpr {
75+
// nested selectors
76+
if expr.expr !is ast.Ident {
77+
vt.save_expr(selectorexpr_cutoff, '${expr.expr.str()}.${expr.field_name}',
78+
vet.file, expr.pos)
79+
}
80+
}
81+
ast.CallExpr {
82+
if expr.is_static_method || expr.is_method {
83+
vt.save_expr(callexpr_cutoff, '${expr.left}.${expr.name}(${expr.args.map(it.str()).join(', ')})',
84+
vet.file, expr.pos)
85+
} else {
86+
vt.save_expr(callexpr_cutoff, '${expr.name}(${expr.args.map(it.str()).join(', ')})',
87+
vet.file, expr.pos)
88+
}
89+
}
90+
ast.AsCast {
91+
vt.save_expr(ascast_cutoff, ast.Expr(expr).str(), vet.file, expr.pos)
92+
}
93+
ast.StringLiteral {
94+
if expr.val.len > stringliteral_min_size {
95+
vt.save_expr(stringliteral_cutoff, ast.Expr(expr).str(), vet.file, expr.pos)
96+
}
97+
}
98+
ast.StringInterLiteral {
99+
vt.save_expr(stringinterliteral_cutoff, ast.Expr(expr).str(), vet.file, expr.pos)
100+
}
101+
else {}
102+
}
103+
}
104+
105+
// long_or_empty_fns checks for long or empty functions
106+
fn (mut vt VetAnalyze) long_or_empty_fns(mut vet Vet, fn_decl ast.FnDecl) {
107+
nr_lines := if fn_decl.stmts.len == 0 {
108+
0
109+
} else {
110+
fn_decl.stmts.last().pos.line_nr - fn_decl.pos.line_nr
111+
}
112+
if nr_lines > long_fns_cutoff {
113+
vet.notice('Long function - ${nr_lines} lines long.', fn_decl.pos.line_nr, .long_fns)
114+
} else if nr_lines == 0 {
115+
vet.notice('Empty function.', fn_decl.pos.line_nr, .empty_fn)
116+
}
117+
}
118+
119+
// vet_fn_analysis reports repeated code by scope
120+
fn (mut vt VetAnalyze) vet_repeated_code(mut vet Vet) {
121+
rlock vt.repeated_expr {
122+
for fn_name, ref_expr in vt.repeated_expr {
123+
scope_name := if fn_name == '' { 'global scope' } else { 'function scope (${fn_name})' }
124+
for expr, info in ref_expr {
125+
occurrences := arrays.sum(info.values().map(it.len)) or { 0 }
126+
if occurrences < vt.repeated_expr_cutoff[expr] {
127+
continue
128+
}
129+
for file, info_pos in info {
130+
for k, pos in info_pos {
131+
vet.notice_with_file(file, '${expr} occurs ${k + 1}/${occurrences} times in ${scope_name}.',
132+
pos.line_nr, .repeated_code)
133+
}
134+
}
135+
}
136+
}
137+
}
138+
}
139+
140+
// vet_code_analyze performs code analysis
141+
fn (mut vt Vet) vet_code_analyze() {
142+
if vt.opt.repeated_code {
143+
vt.analyze.vet_repeated_code(mut vt)
144+
}
145+
}

‎cmd/tools/vvet/errors.v‎

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,17 @@ pub enum FixKind {
1313
unknown
1414
doc
1515
vfmt
16+
repeated_code
17+
long_fns
18+
empty_fn
1619
}
1720

1821
// ErrorType is used to filter out false positive errors under specific conditions
1922
pub enum ErrorType {
2023
default
2124
space_indent
2225
trailing_space
26+
long_fns
2327
}
2428

2529
@[minify]
@@ -40,13 +44,15 @@ fn (mut vt Vet) error(msg string, line int, fix FixKind) {
4044
pos := token.Pos{
4145
line_nr: line + 1
4246
}
43-
vt.errors << VetError{
44-
message: msg
45-
file_path: vt.file
46-
pos: pos
47-
kind: .error
48-
fix: fix
49-
typ: .default
47+
lock vt.errors {
48+
vt.errors << VetError{
49+
message: msg
50+
file_path: vt.file
51+
pos: pos
52+
kind: .error
53+
fix: fix
54+
typ: .default
55+
}
5056
}
5157
}
5258

@@ -64,23 +70,45 @@ fn (mut vt Vet) warn(msg string, line int, fix FixKind) {
6470
}
6571
if vt.opt.is_werror {
6672
w.kind = .error
67-
vt.errors << w
73+
lock vt.errors {
74+
vt.errors << w
75+
}
6876
} else {
69-
vt.warns << w
77+
lock vt.warns {
78+
vt.warns << w
79+
}
7080
}
7181
}
7282

7383
fn (mut vt Vet) notice(msg string, line int, fix FixKind) {
7484
pos := token.Pos{
7585
line_nr: line + 1
7686
}
77-
vt.notices << VetError{
78-
message: msg
79-
file_path: vt.file
80-
pos: pos
81-
kind: .notice
82-
fix: fix
83-
typ: .default
87+
lock vt.notices {
88+
vt.notices << VetError{
89+
message: msg
90+
file_path: vt.file
91+
pos: pos
92+
kind: .notice
93+
fix: fix
94+
typ: .default
95+
}
96+
}
97+
}
98+
99+
fn (mut vt Vet) notice_with_file(file string, msg string, line int, fix FixKind) {
100+
pos := token.Pos{
101+
line_nr: line + 1
102+
}
103+
lock vt.notices {
104+
vt.notices << VetError{
105+
message: msg
106+
file_path: file
107+
pos: pos
108+
kind: .notice
109+
fix: fix
110+
typ: .default
111+
}
84112
}
85113
}
86114

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
cmd/tools/vvet/tests/empty_fn_decl.vv:2: notice: Empty function.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// vtest vflags: -F
2+
fn foo() {
3+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
cmd/tools/vvet/tests/repeated_assign.vv:4: notice: a += 'foo' occurs 1/11 times in function scope (main.main).
2+
cmd/tools/vvet/tests/repeated_assign.vv:5: notice: a += 'foo' occurs 2/11 times in function scope (main.main).
3+
cmd/tools/vvet/tests/repeated_assign.vv:6: notice: a += 'foo' occurs 3/11 times in function scope (main.main).
4+
cmd/tools/vvet/tests/repeated_assign.vv:7: notice: a += 'foo' occurs 4/11 times in function scope (main.main).
5+
cmd/tools/vvet/tests/repeated_assign.vv:8: notice: a += 'foo' occurs 5/11 times in function scope (main.main).
6+
cmd/tools/vvet/tests/repeated_assign.vv:9: notice: a += 'foo' occurs 6/11 times in function scope (main.main).
7+
cmd/tools/vvet/tests/repeated_assign.vv:10: notice: a += 'foo' occurs 7/11 times in function scope (main.main).
8+
cmd/tools/vvet/tests/repeated_assign.vv:11: notice: a += 'foo' occurs 8/11 times in function scope (main.main).
9+
cmd/tools/vvet/tests/repeated_assign.vv:12: notice: a += 'foo' occurs 9/11 times in function scope (main.main).
10+
cmd/tools/vvet/tests/repeated_assign.vv:13: notice: a += 'foo' occurs 10/11 times in function scope (main.main).
11+
cmd/tools/vvet/tests/repeated_assign.vv:14: notice: a += 'foo' occurs 11/11 times in function scope (main.main).
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// vtest vflags: -r
2+
fn main() {
3+
mut a := ''
4+
a += 'foo'
5+
a += 'foo'
6+
a += 'foo'
7+
a += 'foo'
8+
a += 'foo'
9+
a += 'foo'
10+
a += 'foo'
11+
a += 'foo'
12+
a += 'foo'
13+
a += 'foo'
14+
a += 'foo'
15+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
cmd/tools/vvet/tests/repeated_code.vv:4: notice: a[0] occurs 1/12 times in function scope (main.main).
2+
cmd/tools/vvet/tests/repeated_code.vv:5: notice: a[0] occurs 2/12 times in function scope (main.main).
3+
cmd/tools/vvet/tests/repeated_code.vv:6: notice: a[0] occurs 3/12 times in function scope (main.main).
4+
cmd/tools/vvet/tests/repeated_code.vv:6: notice: a[0] occurs 4/12 times in function scope (main.main).
5+
cmd/tools/vvet/tests/repeated_code.vv:7: notice: a[0] occurs 5/12 times in function scope (main.main).
6+
cmd/tools/vvet/tests/repeated_code.vv:8: notice: a[0] occurs 6/12 times in function scope (main.main).
7+
cmd/tools/vvet/tests/repeated_code.vv:9: notice: a[0] occurs 7/12 times in function scope (main.main).
8+
cmd/tools/vvet/tests/repeated_code.vv:9: notice: a[0] occurs 8/12 times in function scope (main.main).
9+
cmd/tools/vvet/tests/repeated_code.vv:11: notice: a[0] occurs 9/12 times in function scope (main.main).
10+
cmd/tools/vvet/tests/repeated_code.vv:12: notice: a[0] occurs 10/12 times in function scope (main.main).
11+
cmd/tools/vvet/tests/repeated_code.vv:13: notice: a[0] occurs 11/12 times in function scope (main.main).
12+
cmd/tools/vvet/tests/repeated_code.vv:13: notice: a[0] occurs 12/12 times in function scope (main.main).
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// vtest vflags: -r
2+
fn main() {
3+
mut a := [0, 2, 4, 6]
4+
if a[0] {
5+
dump(a[0])
6+
dump(a[0] + a[0])
7+
if a[0] {
8+
dump(a[0])
9+
dump(a[0] + a[0])
10+
}
11+
if a[0] {
12+
dump(a[0])
13+
dump(a[0] + a[0])
14+
}
15+
}
16+
}

‎cmd/tools/vvet/vet_test.v‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,22 @@ import term
33
import v.util.vtest
44
import v.util.diff
55

6+
struct FileOptions {
7+
mut:
8+
vflags string
9+
}
10+
11+
fn get_file_options(file string) FileOptions {
12+
mut res := FileOptions{}
13+
lines := os.read_lines(file) or { [] }
14+
for line in lines {
15+
if line.starts_with('// vtest vflags:') {
16+
res.vflags = line.all_after(':').trim_space()
17+
}
18+
}
19+
return res
20+
}
21+
622
fn test_vet() {
723
vexe := os.getenv('VEXE')
824
vroot := os.dir(vexe)
@@ -26,7 +42,8 @@ fn check_path(vexe string, dir string, tests []string) int {
2642
for path in paths {
2743
program := path
2844
print(path + ' ')
29-
res := os.execute('${os.quoted_path(vexe)} vet -nocolor ${os.quoted_path(program)}')
45+
file_options := get_file_options(path)
46+
res := os.execute('${os.quoted_path(vexe)} vet -nocolor ${file_options.vflags} ${os.quoted_path(program)}')
3047
if res.exit_code < 0 {
3148
panic(res.output)
3249
}

0 commit comments

Comments
 (0)