dead/def.bend

bend-lint-pack@0.1.1.0 · 199 lines · 16 definitions

1# dead/def: warn about a def that no entry point reaches.
2#
3# Entry points: `main`, every law and the def that proves it (laws are specs, so what they mention
4# is live), and the names in the `entries` option. Everything reachable from an entry through
5# references (in bodies and in types) is live; every other def is dead, so a chain of defs that
6# only call each other is reported whole. Self-references do not count.
7#
8# Bend has no export keyword, so a def meant for outside callers (a library API, a def a JS host
9# calls) is named in `entries`: a comma-separated list of names, where `name*` is a prefix and a
10# name matches with or without its file's namespace (`helper` or `util:helper`). `*` alone makes
11# every def an entry.
12#
13# The program is the linted file and what it imports, so lint the entry file. By default only the
14# linted file's own defs are reported; `imports` reports the defs of every local file of the program
15# (set it when the imports are your own modules and the linted file is the whole application).
16# Not reported: types and constructors, foreign effect defs (they have no checked body), and hub
17# packages. With no `main`, no law and no `entries`, there is nothing to measure from: one hint
18# says so.
19# Options: entries (default ""), imports (default false).
20import Base
21import bend-lint-pack@0.1.1.0/vendor/lint.bend as Lint
22import bend-lint-pack@0.1.1.0/common.bend as Common
23
24def id() -> String:
25  "dead/def"
26
27def facts() -> Lint.Want:
28  Lint.Want{Lint.Program{}, [], [], []}
29
30# Entry points
31# ============
32def is_root(+laws: Set(), +entries: List<&2, String>, +owner: String) -> Bool:
33  Bool.or(
34    String.eq(owner, "main"),
35    Bool.or(Common.Laws.has(laws, owner), Common.Entry.any(entries, owner))
36  )
37
38def roots(
39  seen: List<&2, Common.Seen>,
40  +laws: Set(),
41  +entries: List<&2, String>,
42  +out: List<&2, String>
43) -> List<&2, String>:
44  match seen:
45    case Nil{}:
46      out
47    case Con{Common.Seen{+owner, span}, rest}:
48      roots(
49        rest,
50        laws,
51        entries,
52        Bool.pick(List<&2, String>, is_root(laws, entries, owner), owner <> out, out)
53      )
54
55# Reachability
56# ============
57# The names reachable from `work` through `edges`. Each name is expanded once, and the names are
58# finite, so this terminates; Bend cannot see that, hence @unsafe.
59@unsafe
60def reach(work: List<&2, String>, +edges: Map<&2, List<&2, String>>, +seen: Set()) -> Set():
61  match work:
62    case Nil{}:
63      seen
64    case Con{+name, rest}:
65      reach.go(Common.has(seen, name), name, rest, edges, seen)
66
67def reach.go(
68  known: Bool,
69  +name: String,
70  rest: List<&2, String>,
71  +edges: Map<&2, List<&2, String>>,
72  +seen: Set()
73) -> Set():
74  match known:
75    case True{}:
76      reach(rest, edges, seen)
77    case False{}:
78      reach(
79        List.append(&2, String, Common.Edges.of(edges, name), rest),
80        edges,
81        Set.add(seen, name)
82      )
83
84# Findings
85# ========
86def message(+owner: String, referenced: Bool) -> String:
87  Bool.pick(
88    String,
89    referenced,
90    "def " ++ owner ++ " is dead: only dead defs use it. Delete it, or name it in the `entries` option if code outside this program calls it.",
91    "def " ++ owner ++ " is never referenced. Delete it, or name it in the `entries` option if code outside this program calls it."
92  )
93
94def is_dead(+nodes: Set(), +reached: Set(), +owner: String) -> Bool:
95  Bool.and(Common.has(nodes, owner), Bool.not(Common.has(reached, owner)))
96
97def report(
98  seen: List<&2, Common.Seen>,
99  +nodes: Set(),
100  +reached: Set(),
101  +into: Map<&2, List<&2, String>>,
102  out: List<&2, Lint.Diag>
103) -> List<&2, Lint.Diag>:
104  match seen:
105    case Nil{}:
106      out
107    case Con{Common.Seen{+owner, span}, rest}:
108      report(
109        rest,
110        nodes,
111        reached,
112        into,
113        Common.onto(
114          Common.when(
115            is_dead(nodes, reached, owner),
116            Common.diag(
117              Lint.Warning{},
118              message(
119                owner,
120                Bool.not(List.is_empty(&2, String, Common.Edges.of(into, owner)))
121              ),
122              span
123            )
124          ),
125          out
126        )
127      )
128
129def nothing() -> Lint.Diag:
130  Common.diag(
131    Lint.Hint{},
132    "dead/def has nothing to measure from: the program has no `main`, no law and no `entries` option. Lint the entry file, or set `entries`.",
133    None{}
134  )
135
136# `none`: there is no entry point. `shown`: the owners reported on.
137def verdict.go(
138  none: Bool,
139  +rs: List<&2, String>,
140  shown: List<&2, Common.Seen>,
141  nodes: Set(),
142  +edges: Map<&2, List<&2, String>>,
143  into: Map<&2, List<&2, String>>
144) -> List<&2, Lint.Diag>:
145  match none:
146    case True{}:
147      [nothing()]
148    case False{}:
149      List.reverse(
150        &2,
151        Lint.Diag,
152        report(shown, nodes, reach(rs, edges, Set.new()), into, [])
153      )
154
155def verdict.roots(
156  +rs: List<&2, String>,
157  shown: List<&2, Common.Seen>,
158  nodes: Set(),
159  edges: Map<&2, List<&2, String>>,
160  into: Map<&2, List<&2, String>>
161) -> List<&2, Lint.Diag>:
162  verdict.go(List.is_empty(&2, String, rs), rs, shown, nodes, edges, into)
163
164def verdict.laws(
165  +laws: Set(),
166  entries: String,
167  +seen: List<&2, Common.Seen>,
168  scope: Common.Scope,
169  nodes: Set(),
170  edges: Map<&2, List<&2, String>>,
171  into: Map<&2, List<&2, String>>
172) -> List<&2, Lint.Diag>:
173  verdict.roots(
174    roots(seen, laws, Common.Entry.parse(entries), []),
175    Common.scoped(scope, seen),
176    nodes,
177    edges,
178    into
179  )
180
181def verdict(p: Common.Prog, entries: String, scope: Common.Scope) -> List<&2, Lint.Diag>:
182  match p:
183    case Common.Prog{seen, edges, into, nodes, laws}:
184      verdict.laws(laws, entries, seen, scope, nodes, edges, into)
185
186def run(input: Lint.Input) -> IO(List<&2, Lint.Diag>):
187  match input:
188    case Lint.Input{+sources, +options}:
189      do IO<List<&2, Lint.Diag>>:
190        g : Common.Graph <- Lint.fold_facts(~Common.Graph, ~Common.Graph.collect, Common.Graph.empty())
191        p : Common.Prog <- Common.Prog.of(sources, g)
192        return verdict(
193          p,
194          Lint.option_text(options, "entries", ""),
195          Common.Scope.of(sources, options)
196        )
197
198def main() -> IO(Unit):
199  Lint.serve(run)