complexity/cognitive.bend

bend-lint-pack@0.1.1.0 · 117 lines · 12 definitions

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).
12import Base
13import bend-lint-pack@0.1.1.0/vendor/lint.bend as Lint
14import bend-lint-pack@0.1.1.0/common.bend as Common
15
16def id() -> String:
17  "complexity/cognitive"
18
19def 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
27def walk(n: Lint.Node, depth: U32, chain: Bool, total: U32) -> IO(U32):
28  do IO<U32>:
29    s : Lint.Shape <- Lint.shape(n)
30    walk.shape(s, depth, chain, total)
31
32# Each of `cs` the same way.
33def 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.
41def 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
53def 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
74def 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
79def 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
86def 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
89def 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<List<&2, Lint.Diag>>:
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
109def run(input: Lint.Input) -> IO(List<&2, Lint.Diag>):
110  match input:
111    case Lint.Input{sources, options}:
112      do IO<List<&2, Lint.Diag>>:
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
116def main() -> IO(Unit):
117  Lint.serve(run)