1 # src/rules: every rule, and its group. A rule is a module under 2 # rules// with `check(src) -> List`, run over each file read 3 # once (src/src.bend); a project rule has `check(ds) -> List` and 4 # sees many files at once, as the digests of rules/digest.bend: `coverage` 5 # one package's files, `unsafe` every file (`project` below). The directory 6 # names the group, and codes.bend says it again for the config. 7 # Adding one is adding it here and a row in codes.bend. One rule is not 8 # listed here, `noqa` (rules/style/noqa.bend): it reads the other rules' 9 # graded findings, so the planner runs it after them (lint/plan.bend). Nor 10 # is `setting` (rules/correctness/setting.bend): it reads each bolt.bend that 11 # grades the run, not a source, so the planner runs it between the per-file 12 # and the project rules. 13 # The groups are what a bolt.bend switches at once: correctness (what will 14 # fail or blow up), suspicious (what is probably a slip), style, laws, and 15 # pedantic (advice that is noisy on idiomatic code: off unless a project 16 # asks for it). 17 import Base 18 import ./finding.bend as F 19 import ./codes.bend as Codes 20 import ./src.bend as Src 21 import ./role.bend as Role 22 import ./rules/digest.bend as Digest 23 import ./rules/imports.bend as Imports 24 import ./lazy/lazy.bend as Lazy 25 import ./config.bend as Config 26 import ./rules/style/doc.bend as Doc 27 import ./rules/suspicious/unused.bend as Unused 28 import ./rules/correctness/hole.bend as Hole 29 import ./rules/style/space.bend as Space 30 import ./rules/style/wrap.bend as Wrap 31 import ./rules/style/param.bend as Param 32 import ./rules/style/transcong.bend as Transcong 33 import ./rules/correctness/pick.bend as Pick 34 import ./rules/laws/law.bend as Law 35 import ./rules/suspicious/strict.bend as Strict 36 import ./rules/suspicious/eager.bend as Eager 37 import ./rules/pedantic/tail.bend as Tail 38 import ./rules/suspicious/concat.bend as Concat 39 import ./rules/suspicious/index.bend as Index 40 import ./rules/suspicious/table.bend as Table 41 import ./rules/suspicious/hoist.bend as Hoist 42 import ./rules/suspicious/ring.bend as Ring 43 import ./rules/suspicious/rewalk.bend as Rewalk 44 import ./rules/suspicious/unit.bend as Unit 45 import ./rules/suspicious/scan.bend as Scan 46 import ./rules/suspicious/thunk.bend as Thunk 47 import ./rules/suspicious/fromrev.bend as Fromrev 48 import ./rules/correctness/put.bend as Put 49 import ./rules/correctness/escape.bend as Escape 50 import ./rules/correctness/strings.bend as Strings 51 import ./rules/correctness/chars.bend as Chars 52 import ./rules/correctness/twice.bend as Twice 53 import ./rules/correctness/arms.bend as Arms 54 import ./rules/correctness/foreign.bend as Foreign 55 import ./rules/suspicious/fuel.bend as Fuel 56 import ./rules/laws/closed.bend as Closed 57 import ./rules/laws/unsafe.bend as Unsafe 58 59 # the file a Src was read from 60 def on.path(ss: Src.Src) -> String: 61 Src.Src{path, _text, _toks, _tree, _bound, _items} = ss 62 path 63 64 # findings put at a path: a per-file rule reports on its own file, so this 65 # keeps what each rule gave and makes it hold by construction 66 def on.at(+path: String, fs: List<&2, F.Finding>) -> List<&2, F.Finding>: 67 match fs: 68 case Nil{}: 69 Nil{} 70 case Con{F.Finding{_path, line, col, len, rule, msg}, rest}: 71 F.Finding{path, line, col, len, rule, msg} <> on.at(path, rest) 72 73 # every per-file rule on a file, told its role (src/role.bend): the rules a 74 # law or proof file changes (doc, hole, param, scan, fromrev, closed) read 75 # the role, never the path, and the cost rules check nothing in a law or 76 # proof file 77 def on.rules.role(+rr: Role.Role, +ss: Src.Src) -> List<&2, F.Finding>: 78 List.concat(&2, F.Finding, [ 79 Doc.check.role(rr, ss), 80 Unused.check(ss), 81 Hole.check.role(rr, ss), 82 Space.check(ss), 83 Wrap.check(ss), 84 Param.check.role(rr, ss), 85 Role.skip(rr, _u => Pick.check(ss)), 86 Role.skip(rr, _u => Pick.search(ss)), 87 Role.skip(rr, _u => Strict.check(ss)), 88 Role.skip(rr, _u => Eager.check(ss)), 89 Role.skip(rr, _u => Tail.check(ss)), 90 Role.skip(rr, _u => Concat.check(ss)), 91 Role.skip(rr, _u => Index.check(ss)), 92 Role.skip(rr, _u => Table.check(ss)), 93 Role.skip(rr, _u => Hoist.check(ss)), 94 Role.skip(rr, _u => Ring.check(ss)), 95 Role.skip(rr, _u => Rewalk.check(ss)), 96 Role.skip(rr, _u => Unit.check(ss)), 97 Scan.check.role(rr, ss), 98 Role.skip(rr, _u => Thunk.check(ss)), 99 Fromrev.check.role(rr, ss), 100 Put.check(ss), 101 Escape.check(ss), 102 Strings.check(ss), 103 Chars.check(ss), 104 Twice.check(ss), 105 Arms.check(ss), 106 Foreign.check(ss), 107 Fuel.check(ss), 108 Closed.check.role(rr, ss), 109 Transcong.check(ss)]) 110 111 # every rule's findings on a file read once, as each rule gives them, its 112 # role read from its name 113 def on.rules(+ss: Src.Src) -> List<&2, F.Finding>: 114 on.rules.role(Role.of_path(on.path(ss)), ss) 115 116 # every rule's findings on a file read once, each at that file 117 def on(+ss: Src.Src) -> List<&2, F.Finding>: 118 on.at(on.path(ss), on.rules(ss)) 119 120 # every rule's findings on a file read once, told its role, each at that file 121 def on.role(+rr: Role.Role, +ss: Src.Src) -> List<&2, F.Finding>: 122 on.at(on.path(ss), on.rules.role(rr, ss)) 123 124 # every rule's findings on a file 125 def all(+path: String, +text: String) -> List<&2, F.Finding>: 126 on(Src.of(path, text)) 127 128 # Packages (laws/ScopePackageRootsFound.bend). A package root other than `.` is a directory 129 # holding a main.bend; a file belongs to the nearest root at or above it, and 130 # `coverage` runs on each package's files alone. A directory is 131 # named here as Digest.dir names one: normalized, with its slash (`w3c/`), 132 # and `` for `.`. 133 134 # whether a directory is among the directories 135 def has(ds: List<&2, String>, +dd: String) -> Bool: 136 match ds: 137 case Nil{}: 138 False{} 139 case Con{d, rest}: 140 Lazy.or_else(String.eq(d, dd), _u => has(rest, dd)) 141 142 def ups.go(rs: List<&2, String>) -> List<&2, String>: 143 match rs: 144 case Nil{}: 145 Nil{} 146 case Con{s, +rest}: 147 (String.join(List.reverse(&2, String, s <> rest), "/") ++ "/") <> ups.go(rest) 148 149 # a directory and each of its parents but ``, nearest first: `a/b/` is 150 # `a/b/` then `a/` 151 def ups(dir: String) -> List<&2, String>: 152 ups.go(List.reverse(&2, String, Imports.init(String.split(dir, '/')))) 153 154 # the first of the directories that is a root, or `` (the package `.`) 155 def owner.go(us: List<&2, String>, +roots: List<&2, String>) -> String: 156 match us: 157 case Nil{}: 158 "" 159 case Con{+u, rest}: 160 Lazy.stop(String, has(roots, u), u, _u => owner.go(rest, roots)) 161 162 # the package of a file in a directory: the nearest root at or above it 163 def owner(+roots: List<&2, String>, dir: String) -> String: 164 owner.go(ups(dir), roots) 165 166 # the files of one package, in order 167 def members(ds: List<&2, Digest.Digest>, +roots: List<&2, String>, +pkg: String) -> List<&2, Digest.Digest>: 168 match ds: 169 case Nil{}: 170 Nil{} 171 case Con{Digest.Digest{path, norm, +dir, is_laws, is_law_file, exempt, tops, says, deps, unsafes, laws}, rest}: 172 +more = members(rest, roots, pkg) 173 Bool.pick(List<&2, Digest.Digest>, String.eq(owner(roots, dir), pkg), 174 Digest.Digest{path, norm, dir, is_laws, is_law_file, exempt, tops, says, deps, unsafes, laws} <> more, more) 175 176 # `coverage` on each package but `.` 177 def covers(rs: List<&2, String>, +ds: List<&2, Digest.Digest>, +roots: List<&2, String>) -> List<&2, F.Finding>: 178 match rs: 179 case Nil{}: 180 Nil{} 181 case Con{+dir, rest}: 182 List.append(&2, F.Finding, Law.check(members(ds, roots, dir)), covers(rest, ds, roots)) 183 184 # every project rule's findings on the files and the package roots but `.` 185 # (each a directory as Digest.dir names one): each file is read down to its 186 # digest once (rules/digest.bend); `coverage` runs on each package's files, 187 # `.` first, and `unsafe` on every file 188 def project(+srcs: List<&2, Src.Src>, +rs: List<&2, String>) -> List<&2, F.Finding>: 189 +ds = Digest.all.role(Role.marks(srcs), srcs) 190 List.concat(&2, F.Finding, [Law.check(members(ds, rs, "")), covers(rs, ds, rs), Unsafe.check(ds)]) 191 192 # the group a rule is in; an unknown slug grades with correctness 193 def group(+rule: String) -> String: 194 +named = Codes.group(rule) 195 Bool.pick(String, String.is_empty(named), "correctness", named) 196 197 # the findings at the levels a config gives their rules; the ones set off 198 # are dropped 199 def graded(+cfg: Config.Config, fs: List<&2, F.Finding>) -> List<&2, F.Graded>: 200 match fs: 201 case Nil{}: 202 Nil{} 203 case Con{F.Finding{path, line, col, len, +rule, msg}, rest}: 204 +more = graded(cfg, rest) 205 +l = Config.level(cfg, group(rule), rule) 206 Bool.pick(List<&2, F.Graded>, Config.is_off(l), more, 207 F.Graded{l, F.Finding{path, line, col, len, rule, msg}} <> more)