v / cmd / tools / vvet / analyze.v
199 lines · 184 sloc · 6.49 KB · e2e5cf8db56f3562c7baa735061690be936bdf3e
Raw
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.
3module main
4
5import v.ast
6import v.token
7import os
8import arrays
9
10// cutoffs
11const indexexpr_cutoff = os.getenv_opt('VET_INDEXEXPR_CUTOFF') or { '10' }.int()
12const infixexpr_cutoff = os.getenv_opt('VET_INFIXEXPR_CUTOFF') or { '10' }.int()
13const selectorexpr_cutoff = os.getenv_opt('VET_SELECTOREXPR_CUTOFF') or { '10' }.int()
14const callexpr_cutoff = os.getenv_opt('VET_CALLEXPR_CUTOFF') or { '10' }.int()
15const stringinterliteral_cutoff = os.getenv_opt('STRINGINTERLITERAL_CUTOFF') or { '10' }.int()
16const stringliteral_cutoff = os.getenv_opt('STRINGLITERAL_CUTOFF') or { '10' }.int()
17const ascast_cutoff = os.getenv_opt('ASCAST_CUTOFF') or { '10' }.int()
18const stringconcat_cutoff = os.getenv_opt('STRINGCONCAT_CUTOFF') or { '10' }.int()
19
20// possibly inline fn cutoff
21const fns_call_cutoff = os.getenv_opt('VET_FNS_CALL_CUTOFF') or { '10' }.int() // at least N calls
22const short_fns_cutoff = os.getenv_opt('VET_SHORT_FNS_CUTOFF') or { '3' }.int() // lines
23
24// minimum size for string literals
25const stringliteral_min_size = os.getenv_opt('VET_STRINGLITERAL_MIN_SIZE') or { '20' }.int()
26
27// long functions cutoff
28const long_fns_cutoff = os.getenv_opt('VET_LONG_FNS_CUTOFF') or { '300' }.int()
29
30struct VetAnalyze {
31mut:
32 repeated_expr_cutoff shared map[string]int // repeated code cutoff
33 repeated_expr shared map[string]map[string]map[string][]token.Pos // repeated exprs in fn scope
34 potential_non_inlined shared map[string]map[string]token.Pos // fns might be inlined
35 call_counter shared map[string]int // fn call counter
36 cur_fn ast.FnDecl // current fn declaration
37}
38
39// stmt checks for repeated code in statements
40fn (mut vt VetAnalyze) stmt(vet &Vet, stmt ast.Stmt) {
41 match stmt {
42 ast.AssignStmt {
43 if stmt.op == .plus_assign {
44 if stmt.right[0] in [ast.StringLiteral, ast.StringInterLiteral] {
45 vt.save_expr(stringconcat_cutoff,
46 '${stmt.left[0].str()} += ${stmt.right[0].str()}', vet.file, stmt.pos)
47 }
48 }
49 }
50 else {}
51 }
52}
53
54// save_expr registers a repeated code occurrence
55fn (mut vt VetAnalyze) save_expr(cutoff int, expr string, file string, pos token.Pos) {
56 lock vt.repeated_expr {
57 if vt.cur_fn.name !in vt.repeated_expr {
58 vt.repeated_expr[vt.cur_fn.name] = map[string]map[string][]token.Pos{}
59 }
60 if expr !in vt.repeated_expr[vt.cur_fn.name] {
61 vt.repeated_expr[vt.cur_fn.name][expr] = map[string][]token.Pos{}
62 }
63 if file !in vt.repeated_expr[vt.cur_fn.name][expr] {
64 vt.repeated_expr[vt.cur_fn.name][expr][file] = []token.Pos{}
65 }
66 vt.repeated_expr[vt.cur_fn.name][expr][file] << pos
67 }
68 lock vt.repeated_expr_cutoff {
69 vt.repeated_expr_cutoff[expr] = cutoff
70 }
71}
72
73// exprs checks for repeated code in expressions
74fn (mut vt VetAnalyze) exprs(vet &Vet, exprs []ast.Expr) {
75 for expr in exprs {
76 vt.expr(vet, expr)
77 }
78}
79
80// expr checks for repeated code
81fn (mut vt VetAnalyze) expr(vet &Vet, expr ast.Expr) {
82 match expr {
83 ast.InfixExpr {
84 vt.save_expr(infixexpr_cutoff, '${expr.left} ${expr.op} ${expr.right}', vet.file,
85 expr.pos)
86 }
87 ast.IndexExpr {
88 vt.save_expr(indexexpr_cutoff, '${expr.left}[${expr.index}]', vet.file, expr.pos)
89 }
90 ast.SelectorExpr {
91 // nested selectors
92 if expr.expr !is ast.Ident {
93 vt.save_expr(selectorexpr_cutoff, '${expr.expr.str()}.${expr.field_name}',
94 vet.file, expr.pos)
95 }
96 }
97 ast.CallExpr {
98 if expr.is_static_method || expr.is_method {
99 left_str := expr.left.str()
100 lock vt.call_counter {
101 if vt.cur_fn.receiver.name == left_str {
102 vt.call_counter['${int(vt.cur_fn.receiver.typ)}.${expr.name}']++
103 }
104 }
105 vt.save_expr(callexpr_cutoff,
106 '${left_str}.${expr.name}(${expr.args.map(it.str()).join(', ')})', vet.file,
107 expr.pos)
108 } else {
109 lock vt.call_counter {
110 vt.call_counter[expr.name]++
111 }
112 vt.save_expr(callexpr_cutoff,
113 '${expr.name}(${expr.args.map(it.str()).join(', ')})', vet.file, expr.pos)
114 }
115 }
116 ast.AsCast {
117 vt.save_expr(ascast_cutoff, ast.Expr(expr).str(), vet.file, expr.pos)
118 }
119 ast.StringLiteral {
120 if expr.val.len > stringliteral_min_size {
121 vt.save_expr(stringliteral_cutoff, ast.Expr(expr).str(), vet.file, expr.pos)
122 }
123 }
124 ast.StringInterLiteral {
125 vt.save_expr(stringinterliteral_cutoff, ast.Expr(expr).str(), vet.file, expr.pos)
126 }
127 else {}
128 }
129}
130
131// long_or_empty_fns checks for long or empty functions
132fn (mut vt VetAnalyze) long_or_empty_fns(mut vet Vet, fn_decl ast.FnDecl) {
133 nr_lines := fn_decl.end_pos.line_nr - fn_decl.pos.line_nr - 2
134 if nr_lines > long_fns_cutoff {
135 vet.notice('Long function - ${nr_lines} lines long.', fn_decl.pos.line_nr, .long_fns)
136 } else if nr_lines == 0 {
137 vet.notice('Empty function.', fn_decl.pos.line_nr, .empty_fn)
138 }
139}
140
141// potential_non_inlined checks for potential fns to be inlined
142fn (mut vt VetAnalyze) potential_non_inlined(mut vet Vet, fn_decl ast.FnDecl) {
143 nr_lines := fn_decl.end_pos.line_nr - fn_decl.pos.line_nr - 2
144 if nr_lines < short_fns_cutoff {
145 attr := fn_decl.attrs.find_first('inline')
146 if attr == none {
147 lock vt.potential_non_inlined {
148 vt.potential_non_inlined[fn_decl.fkey()][vet.file] = fn_decl.pos
149 }
150 }
151 }
152}
153
154// vet_fn_analysis reports repeated code by scope
155fn (mut vt VetAnalyze) vet_repeated_code(mut vet Vet) {
156 rlock vt.repeated_expr {
157 for fn_name, ref_expr in vt.repeated_expr {
158 scope_name := if fn_name == '' { 'global scope' } else { 'function scope (${fn_name})' }
159 for expr, info in ref_expr {
160 occurrences := arrays.sum(info.values().map(it.len)) or { 0 }
161 if occurrences < vt.repeated_expr_cutoff[expr] {
162 continue
163 }
164 for file, info_pos in info {
165 for k, pos in info_pos {
166 vet.notice_with_file(file,
167 '${expr} occurs ${k + 1}/${occurrences} times in ${scope_name}.',
168 pos.line_nr, .repeated_code)
169 }
170 }
171 }
172 }
173 }
174}
175
176// vet_inlining_fn reports possible fn to be inlined
177fn (mut vt VetAnalyze) vet_inlining_fn(mut vet Vet) {
178 for fn_name, info in vt.potential_non_inlined {
179 for file, pos in info {
180 calls := vt.call_counter[fn_name] or { 0 }
181 if calls < fns_call_cutoff {
182 continue
183 }
184 vet.notice_with_file(file,
185 '${fn_name.all_after('.')} fn might be inlined (possibly called at least ${calls} times)',
186 pos.line_nr, .inline_fn)
187 }
188 }
189}
190
191// vet_code_analyze performs code analysis
192fn (mut vt Vet) vet_code_analyze() {
193 if vt.opt.repeated_code {
194 vt.analyze.vet_repeated_code(mut vt)
195 }
196 if vt.opt.fn_inlining {
197 vt.analyze.vet_inlining_fn(mut vt)
198 }
199}
200