1 # complexity/cognitive: warn when a def is harder to read than `max` (default 15). 2 # 3 # Nesting-weighted, after SonarSource's cognitive complexity, with Bend's only branch, `match`: 4 # each match adds 1, plus 1 for every match it sits inside (an arm body of an outer match). 5 # Arms of one match do not nest in each other. 6 # 7 # Bend checks a match into a chain of Mat nodes, one per tested constructor: a Mat's first child is 8 # the arm body, and its second is the rest of the chain (another Mat) or the `_` arm (a Lam). 9 # This rule walks that tree and starts a match only at a Mat that is not the rest of a chain. 10 # A match on several values at once (`match a b:`) is checked as nested tests and counts as such. 11 # Options: max (default 15). 12 import Base 13 import ../vendor/lint.bend as Lint 14 import ../common.bend as Common 15 16 def id() -> String: 17 "complexity/cognitive" 18 19 def facts() -> Lint.Want: 20 Lint.Want{Lint.File{}, [], [], []} 21 22 # The tree walk. `depth` is the number of matches around the node, `chain` says the node is the 23 # rest of a match chain (reached from its Mat), and `total` is the score so far. 24 # The checked body is a finite tree, so this terminates; Bend cannot see that (nodes are handles, 25 # not smaller parts), hence @unsafe, as bend-lint's own fold_facts. 26 @unsafe 27 def walk(n: Lint.Node, depth: U32, chain: Bool, total: U32) -> IO(U32): 28 do IO: 29 s : Lint.Shape <- Lint.shape(n) 30 walk.shape(s, depth, chain, total) 31 32 # Each of `cs` the same way. 33 def walk.all(cs: List<&2, Lint.Node>, +depth: U32, +chain: Bool, total: U32) -> IO(U32): 34 match cs: 35 case Nil{}: 36 IO.pure(U32, total) 37 case Con{c, rest}: 38 IO.bind(U32, U32, walk(c, depth, chain, total), t2 => walk.all(rest, depth, chain, t2)) 39 40 # The children of a Mat: the first is an arm body, one level down; the others continue the chain. 41 def walk.arms(cs: List<&2, Lint.Node>, +depth: U32, +first: Bool, total: U32) -> IO(U32): 42 match cs: 43 case Nil{}: 44 IO.pure(U32, total) 45 case Con{c, rest}: 46 IO.bind( 47 U32, 48 U32, 49 walk(c, Bool.pick(U32, first, U32.add(depth, 1), depth), Bool.not(first), total), 50 t2 => walk.arms(rest, depth, False{}, t2) 51 ) 52 53 def walk.step( 54 is_mat: Bool, 55 is_ann: Bool, 56 children: List<&2, Lint.Node>, 57 +depth: U32, 58 +chain: Bool, 59 total: U32 60 ) -> IO(U32): 61 match is_mat is_ann: 62 case True{} _: 63 walk.arms( 64 children, 65 depth, 66 True{}, 67 U32.add(total, Bool.pick(U32, chain, 0, U32.add(depth, 1))) 68 ) 69 case False{} True{}: 70 walk.all(children, depth, chain, total) 71 case False{} False{}: 72 walk.all(children, Bool.pick(U32, chain, U32.add(depth, 1), depth), False{}, total) 73 74 def walk.shape(s: Lint.Shape, depth: U32, chain: Bool, total: U32) -> IO(U32): 75 match s: 76 case Lint.Shape{+kind, name, span, children}: 77 walk.step(String.eq(kind, "Mat"), String.eq(kind, "Ann"), children, depth, chain, total) 78 79 def score.of(body: Maybe<&2, Lint.Node>) -> IO(U32): 80 match body: 81 case None{}: 82 IO.pure(U32, 0) 83 case Some{n}: 84 walk(n, 0, False{}, 0) 85 86 def message(+name: String, score: U32, max: U32) -> String: 87 "def " ++ name ++ " has cognitive complexity " ++ U32.show(score) ++ "; the limit is " ++ U32.show(max) ++ ". Flatten nested matches into helper defs." 88 89 def judge(+max: U32, seen: List<&2, Common.Seen>, out: List<&2, Lint.Diag>) -> IO(List<&2, Lint.Diag>): 90 match seen: 91 case Nil{}: 92 IO.pure(List<&2, Lint.Diag>, List.reverse(&2, Lint.Diag, out)) 93 case Con{Common.Seen{+owner, span}, rest}: 94 do IO>: 95 body : Maybe<&2, Lint.Node> <- Lint.body(owner) 96 +score : U32 <- score.of(body) 97 judge( 98 max, 99 rest, 100 Common.onto( 101 Common.when( 102 U32.is_gt(score, max), 103 Common.diag(Lint.Warning{}, message(owner, score, max), span) 104 ), 105 out 106 ) 107 ) 108 109 def run(input: Lint.Input) -> IO(List<&2, Lint.Diag>): 110 match input: 111 case Lint.Input{sources, options}: 112 do IO>: 113 owners : Common.Owners <- Lint.fold_facts(~Common.Owners, ~Common.Owners.collect, Common.Owners.empty()) 114 judge(Lint.option_number(options, "max", 15), Common.Owners.ordered(owners), []) 115 116 def main() -> IO(Unit): 117 Lint.serve(run)