| 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 os |
| 8 | import arrays |
| 9 | |
| 10 | // cutoffs |
| 11 | const indexexpr_cutoff = os.getenv_opt('VET_INDEXEXPR_CUTOFF') or { '10' }.int() |
| 12 | const infixexpr_cutoff = os.getenv_opt('VET_INFIXEXPR_CUTOFF') or { '10' }.int() |
| 13 | const selectorexpr_cutoff = os.getenv_opt('VET_SELECTOREXPR_CUTOFF') or { '10' }.int() |
| 14 | const callexpr_cutoff = os.getenv_opt('VET_CALLEXPR_CUTOFF') or { '10' }.int() |
| 15 | const stringinterliteral_cutoff = os.getenv_opt('STRINGINTERLITERAL_CUTOFF') or { '10' }.int() |
| 16 | const stringliteral_cutoff = os.getenv_opt('STRINGLITERAL_CUTOFF') or { '10' }.int() |
| 17 | const ascast_cutoff = os.getenv_opt('ASCAST_CUTOFF') or { '10' }.int() |
| 18 | const stringconcat_cutoff = os.getenv_opt('STRINGCONCAT_CUTOFF') or { '10' }.int() |
| 19 | |
| 20 | // possibly inline fn cutoff |
| 21 | const fns_call_cutoff = os.getenv_opt('VET_FNS_CALL_CUTOFF') or { '10' }.int() // at least N calls |
| 22 | const short_fns_cutoff = os.getenv_opt('VET_SHORT_FNS_CUTOFF') or { '3' }.int() // lines |
| 23 | |
| 24 | // minimum size for string literals |
| 25 | const stringliteral_min_size = os.getenv_opt('VET_STRINGLITERAL_MIN_SIZE') or { '20' }.int() |
| 26 | |
| 27 | // long functions cutoff |
| 28 | const long_fns_cutoff = os.getenv_opt('VET_LONG_FNS_CUTOFF') or { '300' }.int() |
| 29 | |
| 30 | struct VetAnalyze { |
| 31 | mut: |
| 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 |
| 40 | fn (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 |
| 55 | fn (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 |
| 74 | fn (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 |
| 81 | fn (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 |
| 132 | fn (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 |
| 142 | fn (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 |
| 155 | fn (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 |
| 177 | fn (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 |
| 192 | fn (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 | |