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). 20 import Base 21 import ../vendor/lint.bend as Lint 22 import ../common.bend as Common 23 24 def id() -> String: 25 "dead/def" 26 27 def facts() -> Lint.Want: 28 Lint.Want{Lint.Program{}, [], [], []} 29 30 # Entry points 31 # ============ 32 def 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 38 def 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 60 def 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 67 def 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 # ======== 86 def 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 94 def is_dead(+nodes: Set(), +reached: Set(), +owner: String) -> Bool: 95 Bool.and(Common.has(nodes, owner), Bool.not(Common.has(reached, owner))) 96 97 def 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 129 def 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. 137 def 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 155 def 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 164 def 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 181 def 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 186 def run(input: Lint.Input) -> IO(List<&2, Lint.Diag>): 187 match input: 188 case Lint.Input{+sources, +options}: 189 do IO>: 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 198 def main() -> IO(Unit): 199 Lint.serve(run)