v / cmd / tools / vvet / vvet.v
591 lines · 561 sloc · 14.82 KB · 8e35f4d9848f7ad35d857a187dddbfd2eca5e19d
Raw
1// Copyright (c) 2019-2023 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license that can be found in the LICENSE file.
3module main
4
5import os
6import os.cmdline
7import v.pref
8import v.parser
9import v.ast
10import v.help
11import term
12import arrays
13
14@[heap]
15struct Vet {
16mut:
17 opt Options
18 errors shared []VetError
19 warns shared []VetError
20 notices shared []VetError
21 file string
22 mod string
23 filtered_lines FilteredLines
24 analyze VetAnalyze
25 regex_vars map[string]bool
26}
27
28struct Options {
29 is_force bool
30 is_werror bool
31 is_verbose bool
32 show_warnings bool
33 use_color bool
34 doc_private_fns_too bool
35 fn_sizing bool
36 repeated_code bool
37 fn_inlining bool
38mut:
39 is_vfmt_off bool
40}
41
42const term_colors = term.can_show_color_on_stderr()
43const clean_seq = ['[', '', ']', '', ' ', '']
44const exclude_dirs = ['test', 'slow_test', 'testdata']
45
46fn main() {
47 vet_options := cmdline.options_after(os.args, ['vet'])
48 mut vt := Vet{
49 opt: Options{
50 is_werror: '-W' in vet_options
51 is_verbose: '-verbose' in vet_options || '-v' in vet_options
52 show_warnings: '-hide-warnings' !in vet_options && '-w' !in vet_options
53 doc_private_fns_too: '-p' in vet_options
54 use_color: '-color' in vet_options
55 || (term_colors && '-nocolor' !in vet_options)
56 repeated_code: '-r' in vet_options
57 fn_sizing: '-F' in vet_options
58 fn_inlining: '-I' in vet_options
59 }
60 }
61 mut paths := cmdline.only_non_options(vet_options)
62 vtmp := os.getenv('VTMP')
63 if vtmp != '' {
64 // `v test-cleancode` passes also `-o tmpfolder` as well as all options in VFLAGS
65 paths = paths.filter(!it.starts_with(vtmp))
66 }
67 if paths.len == 0 || '-help' in vet_options || '--help' in vet_options {
68 help.print_and_exit('vet')
69 }
70 for path in paths {
71 if !os.exists(path) {
72 eprintln('File/folder ${path} does not exist')
73 continue
74 }
75 if os.is_file(path) {
76 vt.vet_file(path)
77 }
78 if os.is_dir(path) {
79 vt.vprintln("vetting folder: '${path}' ...")
80 overwrite_exclude := exclude_dirs.any(path.contains(it))
81 os.walk(path, fn [mut vt, overwrite_exclude] (p string) {
82 if p.ends_with('.v') || p.ends_with('.vv') {
83 if !overwrite_exclude {
84 for d in exclude_dirs {
85 if p.contains(d) {
86 return
87 }
88 }
89 }
90 vt.vet_file(p)
91 }
92 })
93 }
94 }
95 vt.vet_code_analyze()
96 vfmt_err_count := vt.errors.filter(it.fix == .vfmt).len
97 for n in vt.notices {
98 eprintln(vt.e2string(n))
99 }
100 if vt.opt.show_warnings {
101 for w in vt.warns {
102 eprintln(vt.e2string(w))
103 }
104 }
105 for err in vt.errors {
106 eprintln(vt.e2string(err))
107 }
108 if vfmt_err_count > 0 {
109 rlock vt.errors {
110 filtered_out := arrays.distinct(vt.errors.map(it.file_path))
111 eprintln('Note: You can run `v fmt -w ${filtered_out.join(' ')}` to fix these errors automatically')
112 }
113 }
114 if vt.errors.len > 0 {
115 exit(1)
116 }
117}
118
119// vet_file vets the file read from `path`.
120fn (mut vt Vet) vet_file(path string) {
121 vt.file = path
122 mut prefs := pref.new_preferences()
123 prefs.is_vet = true
124 prefs.is_vsh = path.ends_with('.vsh')
125 mut table := ast.new_table()
126 vt.vprintln("vetting file '${path}'...")
127 file := parser.parse_file(path, mut table, .parse_comments, prefs)
128 vt.mod = file.mod.name
129 vt.regex_vars = map[string]bool{}
130 vt.stmts(file.stmts)
131 source_lines := os.read_lines(vt.file) or { []string{} }
132 for ln, line in source_lines {
133 vt.vet_line(source_lines, line, ln)
134 }
135}
136
137// vet_line vets the contents of `line` from `vet.file`.
138fn (mut vt Vet) vet_line(lines []string, line string, lnumber int) {
139 if line == '' {
140 return
141 }
142 vt.vet_fn_documentation(lines, line, lnumber)
143 vt.vet_space_usage(line, lnumber)
144}
145
146fn (mut vt Vet) vet_space_usage(line string, lnumber int) {
147 if line.starts_with('// vfmt off') {
148 vt.opt.is_vfmt_off = true
149 } else if line.starts_with('// vfmt on') {
150 vt.opt.is_vfmt_off = false
151 }
152 if vt.opt.is_vfmt_off {
153 return
154 }
155 if lnumber !in vt.filtered_lines[.space_indent] {
156 if line.starts_with(' ') {
157 vt.error('Looks like you are using spaces for indentation.', lnumber, .vfmt)
158 }
159 }
160 if lnumber !in vt.filtered_lines[.trailing_space] {
161 if line.ends_with(' ') {
162 vt.error('Looks like you have trailing whitespace.', lnumber, .unknown)
163 }
164 }
165}
166
167fn collect_tags(line string) []string {
168 mut cleaned := line.all_before('/')
169 cleaned = cleaned.replace_each(clean_seq)
170 return cleaned.split(',')
171}
172
173fn ident_fn_name(line string) string {
174 mut fn_idx := line.index(' fn ') or { return '' }
175 if line.len < fn_idx + 5 {
176 return ''
177 }
178 mut tokens := line[fn_idx + 4..].split(' ')
179 // Skip struct identifier
180 if tokens.first().starts_with('(') {
181 fn_idx = line.index(')') or { return '' }
182 tokens = line[fn_idx..].split(' ')
183 if tokens.len > 1 {
184 tokens = [tokens[1]]
185 }
186 }
187 if tokens.len > 0 {
188 function_name_with_generic_parameters := tokens[0].all_before('(')
189 return function_name_with_generic_parameters.all_before('[')
190 }
191 return ''
192}
193
194// vet_fn_documentation ensures that functions are documented
195fn (mut vt Vet) vet_fn_documentation(lines []string, line string, lnumber int) {
196 if line.starts_with('fn C.') {
197 return
198 }
199 is_pub_fn := line.starts_with('pub fn ')
200 is_fn := is_pub_fn || line.starts_with('fn ')
201 if !is_fn {
202 return
203 }
204 if line.starts_with('fn main') {
205 return
206 }
207 if !(is_pub_fn || vt.opt.doc_private_fns_too) {
208 return
209 }
210 // Scan function declarations for missing documentation
211 mut line_above := lines[lnumber - 1] or { return }
212 mut tags := []string{}
213 if !line_above.starts_with('//') {
214 mut grab := true
215 for j := lnumber - 1; j >= 0; j-- {
216 prev_line := lines[j]
217 if prev_line.contains('}') { // We've looked back to the above scope, stop here
218 break
219 } else if prev_line.starts_with('@[') {
220 tags << collect_tags(prev_line)
221 continue
222 } else if prev_line.starts_with('//') { // Single-line comment
223 grab = false
224 break
225 }
226 }
227 if grab {
228 clean_line := line.all_before_last('{').trim(' ')
229 vt.warn('Function documentation seems to be missing for "${clean_line}".', lnumber,
230 .doc)
231 }
232 } else {
233 fn_name := ident_fn_name(line)
234 mut grab := true
235 for j := lnumber - 1; j >= 0; j-- {
236 mut prev_prev_line := ''
237 if j - 1 >= 0 {
238 prev_prev_line = lines[j - 1]
239 }
240 prev_line := lines[j]
241
242 if prev_line.starts_with('//') {
243 if prev_line.starts_with('// ${fn_name} ') {
244 grab = false
245 break
246 } else if prev_line.starts_with('// ${fn_name}')
247 && !prev_prev_line.starts_with('//') {
248 grab = false
249 clean_line := line.all_before_last('{').trim(' ')
250 vt.warn('The documentation for "${clean_line}" seems incomplete.', lnumber,
251 .doc)
252 break
253 }
254
255 continue
256 }
257
258 if prev_line.contains('}') { // We've looked back to the above scope, stop here
259 break
260 } else if prev_line.starts_with('@[') {
261 tags << collect_tags(prev_line)
262 continue
263 }
264 }
265 if grab {
266 clean_line := line.all_before_last('{').trim(' ')
267 vt.warn('A function name is missing from the documentation of "${clean_line}".',
268 lnumber, .doc)
269 }
270 }
271}
272
273fn (mut vt Vet) stmts(stmts []ast.Stmt) {
274 for stmt in stmts {
275 vt.stmt(stmt)
276 }
277}
278
279fn (mut vt Vet) stmt(stmt ast.Stmt) {
280 match stmt {
281 ast.ConstDecl {
282 vt.const_decl(stmt)
283 }
284 ast.ExprStmt {
285 vt.expr(stmt.expr)
286 }
287 ast.Return {
288 vt.exprs(stmt.exprs)
289 }
290 ast.AssertStmt {
291 vt.expr(stmt.expr)
292 vt.expr(stmt.extra)
293 }
294 ast.AssignStmt {
295 vt.exprs(stmt.left)
296 vt.exprs(stmt.right)
297 vt.track_regex_assign(stmt)
298 vt.analyze.stmt(&vt, stmt)
299 }
300 ast.FnDecl {
301 old_fn_decl := vt.analyze.cur_fn
302 old_regex_vars := vt.regex_vars.clone()
303 vt.regex_vars = map[string]bool{}
304 vt.analyze.cur_fn = stmt
305 vt.stmts(stmt.stmts)
306 if vt.opt.fn_sizing {
307 vt.analyze.long_or_empty_fns(mut vt, stmt)
308 }
309 if vt.opt.fn_inlining {
310 vt.analyze.potential_non_inlined(mut vt, stmt)
311 }
312 vt.analyze.cur_fn = old_fn_decl
313 vt.regex_vars = old_regex_vars.clone()
314 }
315 ast.StructDecl {
316 vt.exprs(stmt.fields.map(it.default_expr))
317 }
318 else {}
319 }
320}
321
322fn (mut vt Vet) exprs(exprs []ast.Expr) {
323 for expr in exprs {
324 vt.expr(expr)
325 }
326}
327
328fn (mut vt Vet) expr(expr ast.Expr) {
329 match expr {
330 ast.Comment {
331 vt.filtered_lines.comments(expr.is_multi, expr.pos)
332 }
333 ast.StringLiteral {
334 vt.filtered_lines.assigns(expr.pos)
335 vt.analyze.expr(&vt, expr)
336 }
337 ast.StringInterLiteral {
338 vt.filtered_lines.assigns(expr.pos)
339 vt.analyze.expr(&vt, expr)
340 }
341 ast.ArrayInit {
342 vt.filtered_lines.assigns(expr.pos)
343 vt.expr(expr.len_expr)
344 vt.expr(expr.cap_expr)
345 vt.expr(expr.init_expr)
346 vt.exprs(expr.exprs)
347 }
348 ast.InfixExpr {
349 vt.vet_in_condition(expr)
350 vt.vet_empty_str(expr)
351 vt.expr(expr.left)
352 vt.expr(expr.right)
353 vt.analyze.expr(&vt, expr)
354 }
355 ast.ParExpr {
356 vt.expr(expr.expr)
357 }
358 ast.CallExpr {
359 vt.expr(expr.left)
360 vt.exprs(expr.args.map(it.expr))
361 vt.vet_confusing_regex(expr)
362 vt.analyze.expr(&vt, expr)
363 }
364 ast.MatchExpr {
365 vt.expr(expr.cond)
366 for b in expr.branches {
367 vt.exprs(b.exprs)
368 vt.stmts(b.stmts)
369 }
370 }
371 ast.IfExpr {
372 for b in expr.branches {
373 vt.expr(b.cond)
374 vt.stmts(b.stmts)
375 }
376 }
377 ast.SelectorExpr {
378 vt.analyze.expr(&vt, expr)
379 }
380 ast.IndexExpr {
381 vt.analyze.expr(&vt, expr)
382 }
383 ast.AsCast {
384 vt.analyze.expr(&vt, expr)
385 vt.expr(expr.expr)
386 }
387 ast.UnsafeExpr {
388 vt.expr(expr.expr)
389 }
390 ast.CastExpr {
391 vt.expr(expr.expr)
392 }
393 ast.StructInit {
394 vt.expr(expr.update_expr)
395 vt.exprs(expr.init_fields.map(it.expr))
396 }
397 ast.DumpExpr {
398 vt.expr(expr.expr)
399 }
400 else {}
401 }
402}
403
404fn (mut vt Vet) const_decl(stmt ast.ConstDecl) {
405 for field in stmt.fields {
406 if field.expr is ast.ArrayInit && !field.expr.is_fixed {
407 vt.notice('Use a fixed array instead of a dynamic one', field.expr.pos.line_nr,
408 .unknown)
409 }
410 vt.expr(field.expr)
411 }
412}
413
414fn (mut vt Vet) track_regex_assign(stmt ast.AssignStmt) {
415 for i, left in stmt.left {
416 if i >= stmt.right.len {
417 break
418 }
419 mut ident_name := ''
420 match left {
421 ast.Ident {
422 ident_name = left.name
423 }
424 else {
425 continue
426 }
427 }
428
429 if vt.is_regex_value_expr(stmt.right[i]) {
430 vt.regex_vars[ident_name] = true
431 } else {
432 vt.regex_vars.delete(ident_name)
433 }
434 }
435}
436
437fn (mut vt Vet) vet_confusing_regex(expr ast.CallExpr) {
438 if expr.args.len == 0 || !vt.is_regex_pattern_call(expr) {
439 return
440 }
441 pattern_expr := expr.args[0].expr
442 pattern := if pattern_expr is ast.StringLiteral { pattern_expr } else { return }
443 snippet, suggestion := confusing_regex_branch(pattern.val) or { return }
444 vt.warn('Confusing regex `|` in `${snippet}`: V regex applies `|` to adjacent tokens, not whole branches. Use `${suggestion}` if you intended alternation.',
445 pattern.pos.line_nr, .unknown)
446}
447
448fn (vt &Vet) is_regex_pattern_call(expr ast.CallExpr) bool {
449 short_name := expr.name.all_after_last('.')
450 if short_name in ['regex_opt', 'regex_base'] {
451 return vt.is_regex_fn_call(expr)
452 }
453 if expr.name == 'compile_opt' && expr.is_method {
454 return vt.is_regex_value_expr(expr.left)
455 }
456 return false
457}
458
459fn (vt &Vet) is_regex_value_expr(expr ast.Expr) bool {
460 match expr {
461 ast.CallExpr {
462 short_name := expr.name.all_after_last('.')
463 if short_name == 'new' {
464 return vt.is_regex_fn_call(expr)
465 }
466 if short_name == 'regex_opt' {
467 return vt.is_regex_fn_call(expr)
468 }
469 return false
470 }
471 ast.Ident {
472 return expr.name in vt.regex_vars
473 }
474 else {
475 return false
476 }
477 }
478}
479
480fn (vt &Vet) is_regex_fn_call(expr ast.CallExpr) bool {
481 if expr.name in ['regex.regex_opt', 'regex.regex_base', 'regex.new'] {
482 return true
483 }
484 if vt.mod == 'regex' && expr.name in ['regex_opt', 'regex_base', 'new'] {
485 return true
486 }
487 return false
488}
489
490fn confusing_regex_branch(pattern string) ?(string, string) {
491 mut escaped := false
492 mut in_char_class := false
493 for i := 0; i < pattern.len; i++ {
494 ch := pattern[i]
495 if escaped {
496 escaped = false
497 continue
498 }
499 if ch == `\\` {
500 escaped = true
501 continue
502 }
503 if in_char_class {
504 if ch == `]` {
505 in_char_class = false
506 }
507 continue
508 }
509 if ch == `[` {
510 in_char_class = true
511 continue
512 }
513 if ch != `|` || i == 0 || i + 1 >= pattern.len {
514 continue
515 }
516 if !is_regex_plain_letter(pattern[i - 1]) || !is_regex_plain_letter(pattern[i + 1]) {
517 continue
518 }
519 if !((i >= 2 && is_regex_plain_letter(pattern[i - 2]))
520 || (i + 2 < pattern.len && is_regex_plain_letter(pattern[i + 2]))) {
521 continue
522 }
523 mut left_start := i - 1
524 for left_start > 0 && is_regex_plain_letter(pattern[left_start - 1]) {
525 left_start--
526 }
527 mut right_end := i + 1
528 for right_end + 1 < pattern.len && is_regex_plain_letter(pattern[right_end + 1]) {
529 right_end++
530 }
531 left := pattern[left_start..i]
532 right := pattern[i + 1..right_end + 1]
533 return pattern[left_start..right_end + 1], '(${left})|(${right})'
534 }
535 return none
536}
537
538fn is_regex_plain_letter(ch u8) bool {
539 return ch.is_letter()
540}
541
542fn (mut vt Vet) vet_empty_str(expr ast.InfixExpr) {
543 if expr.left is ast.SelectorExpr && expr.right is ast.IntegerLiteral {
544 operand := (expr.left as ast.SelectorExpr) // TODO: remove as-casts when multiple conds can be smart-casted.
545 if operand.expr is ast.Ident && operand.field_name == 'len'
546 && operand.expr.info.typ == ast.string_type_idx {
547 if expr.op != .lt && expr.right.val == '0' {
548 // Case: `var.len > 0`, `var.len == 0`, `var.len != 0`
549 op := if expr.op == .gt { '!=' } else { expr.op.str() }
550 vt.notice("Use `${operand.expr.name} ${op} ''` instead of `${operand.expr.name}.len ${expr.op} 0`",
551 expr.pos.line_nr, .unknown)
552 } else if expr.op == .lt && expr.right.val == '1' {
553 // Case: `var.len < 1`
554 vt.notice("Use `${operand.expr.name} == ''` instead of `${operand.expr.name}.len ${expr.op} 1`",
555 expr.pos.line_nr, .unknown)
556 }
557 }
558 } else if expr.left is ast.IntegerLiteral && expr.right is ast.SelectorExpr {
559 operand := expr.right
560 if operand.expr is ast.Ident && operand.expr.info.typ == ast.string_type_idx
561 && operand.field_name == 'len' {
562 if expr.op != .gt && (expr.left as ast.IntegerLiteral).val == '0' {
563 // Case: `0 < var.len`, `0 == var.len`, `0 != var.len`
564 op := if expr.op == .lt { '!=' } else { expr.op.str() }
565 vt.notice("Use `'' ${op} ${operand.expr.name}` instead of `0 ${expr.op} ${operand.expr.name}.len`",
566 expr.pos.line_nr, .unknown)
567 } else if expr.op == .gt && (expr.left as ast.IntegerLiteral).val == '1' {
568 // Case: `1 > var.len`
569 vt.notice("Use `'' == ${operand.expr.name}` instead of `1 ${expr.op} ${operand.expr.name}.len`",
570 expr.pos.line_nr, .unknown)
571 }
572 }
573 }
574}
575
576fn (vt &Vet) vprintln(s string) {
577 if !vt.opt.is_verbose {
578 return
579 }
580 println(s)
581}
582
583fn (mut vt Vet) vet_in_condition(expr ast.InfixExpr) {
584 if expr.right is ast.ArrayInit && expr.right.exprs.len == 1 && expr.op in [.key_in, .not_in] {
585 left := expr.left.str()
586 right := expr.right.exprs[0].str()
587 eq := if expr.op == .key_in { '==' } else { '!=' }
588 vt.error('Use `${left} ${eq} ${right}` instead of `${left} ${expr.op} [${right}]`',
589 expr.pos.line_nr, .vfmt)
590 }
591}
592