shape.odin (2904B)
1 package check 2 3 // A function's size and depth are measured, not judged. The thresholds 4 // were read off 8,255 functions: length sits between the 95th percentile 5 // (109 lines) and the 99th (257); depth at the 99th (6). 6 7 import "core:fmt" 8 9 import "../finding" 10 11 // max_function_lines is the length past which a function is several; 12 // max_nesting the block depth past which a reader is holding more 13 // context than the function's name gave them. 14 max_function_lines :: 150 15 max_nesting :: 5 16 17 // check_shape reports a new function that is too long or too deeply 18 // nested to read as one thing. 19 check_shape :: proc(s: Scope, out: ^[dynamic]finding.Finding) { 20 for sym in s.c.symbols { 21 if sym.kind != "func" || sym.body == "" { 22 continue 23 } 24 if n := line_count(sym.body); n > max_function_lines { 25 append( 26 out, 27 static( 28 "function-too-long", 29 .Consider, 30 fmt.aprintf( 31 "%s is %d lines, over the %d past which a function is several; 95%% of measured functions fit in 109", 32 sym.name, 33 n, 34 max_function_lines, 35 ), 36 "split it at the point where the reader has to remember what came before", 37 file = sym.file, 38 line = sym.line, 39 symbol = sym.name, 40 ), 41 ) 42 } 43 if d := nesting(sym.body); d > max_nesting { 44 append( 45 out, 46 static( 47 "nesting-too-deep", 48 .Consider, 49 fmt.aprintf( 50 "%s nests %d blocks deep, over the %d past which a reader is holding more than the name told them; 99%% of measured functions stay within 6", 51 sym.name, 52 d, 53 max_nesting, 54 ), 55 "return early, or lift the inner blocks into functions of their own", 56 file = sym.file, 57 line = sym.line, 58 symbol = sym.name, 59 ), 60 ) 61 } 62 } 63 } 64 65 line_count :: proc(body: string) -> int { 66 n := 1 67 for i in 0 ..< len(body) { 68 if body[i] == '\n' { 69 n += 1 70 } 71 } 72 return n 73 } 74 75 // nesting is the deepest block within a body, counted by braces with 76 // strings and comments read through, less the body's own pair. 77 nesting :: proc(body: string) -> int { 78 depth, deepest := 0, 0 79 in_string, in_raw, in_line, in_block: bool 80 quote: byte 81 i := 0 82 for i < len(body) { 83 c := body[i] 84 switch { 85 case in_line: 86 if c == '\n' { 87 in_line = false 88 } 89 case in_block: 90 if c == '*' && i + 1 < len(body) && body[i + 1] == '/' { 91 in_block = false 92 i += 1 93 } 94 case in_raw: 95 if c == '`' { 96 in_raw = false 97 } 98 case in_string: 99 if c == '\\' { 100 i += 1 101 } else if c == quote || c == '\n' { 102 in_string = false 103 } 104 case c == '/' && i + 1 < len(body) && body[i + 1] == '/': 105 in_line = true 106 case c == '/' && i + 1 < len(body) && body[i + 1] == '*': 107 in_block = true 108 case c == '`': 109 in_raw = true 110 case c == '"' || c == '\'': 111 in_string, quote = true, c 112 case c == '{': 113 depth += 1 114 deepest = max(deepest, depth) 115 case c == '}': 116 depth -= 1 117 } 118 i += 1 119 } 120 return max(deepest - 1, 0) 121 }