complexity/cyclomatic.bend

bend-lint-pack@0.1.1.0 · 105 lines · 13 definitions

1# complexity/cyclomatic: warn when a def has more than `max` independent paths (default 10).
2#
3# Bend has no loops or `if`; every branch is a `match`. Bend checks a match into one Mat node per
4# tested constructor, closed by an Efq node when the arms are exhaustive. The closing test is not a
5# decision (its failure is impossible), so
6#
7#   complexity = 1 + (Mat nodes - Efq nodes)
8#
9# Three arms give 3; two arms and a `_` give 2 + 1 = 3; a single-constructor destructuring gives 1.
10# A match on several values at once (`match a b:`) is checked as nested tests and counts as such.
11# Options: max (default 10).
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/cyclomatic"
18
19def facts() -> Lint.Want:
20  Lint.Want{Lint.File{}, [], [], []}
21
22# Node kinds seen under a def.
23type Tally is Data:
24  Tally{mats: U32, efqs: U32}
25
26def bump.kind(+kind: String, t: Tally) -> Tally:
27  match t:
28    case Tally{+mats, +efqs}:
29      Bool.pick(
30        Tally,
31        String.eq(kind, "Mat"),
32        Tally{U32.add(mats, 1), efqs},
33        Bool.pick(
34          Tally,
35          String.eq(kind, "Efq"),
36          Tally{mats, U32.add(efqs, 1)},
37          Tally{mats, efqs}
38        )
39      )
40
41# The checked body is a finite tree, so this terminates; Bend cannot see that (nodes are handles, not
42# smaller parts), hence @unsafe, as bend-lint's own fold_facts.
43@unsafe
44def walk(n: Lint.Node, t: Tally) -> IO(Tally):
45  do IO<Tally>:
46    s : Lint.Shape <- Lint.shape(n)
47    walk.shape(s, t)
48
49def walk.all(cs: List<&2, Lint.Node>, t: Tally) -> IO(Tally):
50  match cs:
51    case Nil{}:
52      IO.pure(Tally, t)
53    case Con{c, rest}:
54      IO.bind(Tally, Tally, walk(c, t), t2 => walk.all(rest, t2))
55
56def walk.shape(s: Lint.Shape, t: Tally) -> IO(Tally):
57  match s:
58    case Lint.Shape{kind, name, span, children}:
59      walk.all(children, bump.kind(kind, t))
60
61# 1 + Mat - Efq; never below 1.
62def complexity(t: Tally) -> U32:
63  match t:
64    case Tally{+mats, efqs}:
65      U32.add(1, U32.sub(mats, U32.min(mats, efqs)))
66
67def score.of(body: Maybe<&2, Lint.Node>) -> IO(U32):
68  match body:
69    case None{}:
70      IO.pure(U32, 0)
71    case Some{n}:
72      IO.bind(Tally, U32, walk(n, Tally{0, 0}), t => IO.pure(U32, complexity(t)))
73
74def message(+name: String, score: U32, max: U32) -> String:
75  "def " ++ name ++ " has cyclomatic complexity " ++ U32.show(score) ++ "; the limit is " ++ U32.show(max) ++ ". Split it into smaller defs."
76
77def judge(+max: U32, seen: List<&2, Common.Seen>, out: List<&2, Lint.Diag>) -> IO(List<&2, Lint.Diag>):
78  match seen:
79    case Nil{}:
80      IO.pure(List<&2, Lint.Diag>, List.reverse(&2, Lint.Diag, out))
81    case Con{Common.Seen{+owner, span}, rest}:
82      do IO<List<&2, Lint.Diag>>:
83        body : Maybe<&2, Lint.Node> <- Lint.body(owner)
84        +score : U32 <- score.of(body)
85        judge(
86          max,
87          rest,
88          Common.onto(
89            Common.when(
90              U32.is_gt(score, max),
91              Common.diag(Lint.Warning{}, message(owner, score, max), span)
92            ),
93            out
94          )
95        )
96
97def run(input: Lint.Input) -> IO(List<&2, Lint.Diag>):
98  match input:
99    case Lint.Input{sources, options}:
100      do IO<List<&2, Lint.Diag>>:
101        owners : Common.Owners <- Lint.fold_facts(~Common.Owners, ~Common.Owners.collect, Common.Owners.empty())
102        judge(Lint.option_number(options, "max", 10), Common.Owners.ordered(owners), [])
103
104def main() -> IO(Unit):
105  Lint.serve(run)