src/rules.bend

bolt@1.25.0.0 · 210 lines · 0 definitions

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