review

review patchsets using your default editor
Log | Files | Refs

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 }