src/rules.bend

bolt@1.21.0.0 · 281 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# and `trace` 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.21.0.0/src/finding.bend as F
19import bolt@1.21.0.0/src/codes.bend as Codes
20import bolt@1.21.0.0/src/src.bend as Src
21import bolt@1.21.0.0/src/rules/digest.bend as Digest
22import bolt@1.21.0.0/src/rules/imports.bend as Imports
23import bolt@1.21.0.0/src/lazy/lazy.bend as Lazy
24import bolt@1.21.0.0/src/config.bend as Config
25import bolt@1.21.0.0/src/rules/style/doc.bend as Doc
26import bolt@1.21.0.0/src/rules/suspicious/unused.bend as Unused
27import bolt@1.21.0.0/src/rules/correctness/hole.bend as Hole
28import bolt@1.21.0.0/src/rules/style/space.bend as Space
29import bolt@1.21.0.0/src/rules/style/wrap.bend as Wrap
30import bolt@1.21.0.0/src/rules/style/param.bend as Param
31import bolt@1.21.0.0/src/rules/style/transcong.bend as Transcong
32import bolt@1.21.0.0/src/rules/correctness/pick.bend as Pick
33import bolt@1.21.0.0/src/rules/laws/law.bend as Law
34import bolt@1.21.0.0/src/rules/suspicious/strict.bend as Strict
35import bolt@1.21.0.0/src/rules/suspicious/eager.bend as Eager
36import bolt@1.21.0.0/src/rules/pedantic/tail.bend as Tail
37import bolt@1.21.0.0/src/rules/suspicious/concat.bend as Concat
38import bolt@1.21.0.0/src/rules/suspicious/index.bend as Index
39import bolt@1.21.0.0/src/rules/suspicious/table.bend as Table
40import bolt@1.21.0.0/src/rules/suspicious/hoist.bend as Hoist
41import bolt@1.21.0.0/src/rules/suspicious/ring.bend as Ring
42import bolt@1.21.0.0/src/rules/suspicious/rewalk.bend as Rewalk
43import bolt@1.21.0.0/src/rules/suspicious/unit.bend as Unit
44import bolt@1.21.0.0/src/rules/suspicious/scan.bend as Scan
45import bolt@1.21.0.0/src/rules/suspicious/thunk.bend as Thunk
46import bolt@1.21.0.0/src/rules/suspicious/fromrev.bend as Fromrev
47import bolt@1.21.0.0/src/rules/correctness/put.bend as Put
48import bolt@1.21.0.0/src/rules/correctness/escape.bend as Escape
49import bolt@1.21.0.0/src/rules/correctness/strings.bend as Strings
50import bolt@1.21.0.0/src/rules/correctness/chars.bend as Chars
51import bolt@1.21.0.0/src/rules/correctness/twice.bend as Twice
52import bolt@1.21.0.0/src/rules/correctness/arms.bend as Arms
53import bolt@1.21.0.0/src/rules/correctness/foreign.bend as Foreign
54import bolt@1.21.0.0/src/rules/suspicious/fuel.bend as Fuel
55import bolt@1.21.0.0/src/rules/laws/closed.bend as Closed
56import bolt@1.21.0.0/src/rules/laws/trace.bend as Trace
57import bolt@1.21.0.0/src/rules/laws/unsafe.bend as Unsafe
58
59# the file a Src was read from
60def 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
66def 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 rule's findings on a file read once, as each rule gives them
74def on.rules(+ss: Src.Src) -> List<&2, F.Finding>:
75  List.concat(&2, F.Finding, [
76    Doc.check(ss),
77    Unused.check(ss),
78    Hole.check(ss),
79    Space.check(ss),
80    Wrap.check(ss),
81    Param.check(ss),
82    Pick.check(ss),
83    Strict.check(ss),
84    Eager.check(ss),
85    Tail.check(ss),
86    Concat.check(ss),
87    Index.check(ss),
88    Table.check(ss),
89    Hoist.check(ss),
90    Ring.check(ss),
91    Rewalk.check(ss),
92    Unit.check(ss),
93    Scan.check(ss),
94    Thunk.check(ss),
95    Fromrev.check(ss),
96    Put.check(ss),
97    Escape.check(ss),
98    Strings.check(ss),
99    Chars.check(ss),
100    Twice.check(ss),
101    Arms.check(ss),
102    Foreign.check(ss),
103    Fuel.check(ss),
104    Closed.check(ss),
105    Transcong.check(ss)])
106
107# every rule's findings on a file read once, each at that file
108def on(+ss: Src.Src) -> List<&2, F.Finding>:
109  on.at(on.path(ss), on.rules(ss))
110
111# every rule's findings on a file
112def all(+path: String, +text: String) -> List<&2, F.Finding>:
113  on(Src.of(path, text))
114
115# Packages (BOLT-SCOPE-6). A package root other than `.` is a directory
116# holding a main.bend; a file belongs to the nearest root at or above it, and
117# `coverage` and `trace` run on each package's files alone. A directory is
118# named here as Digest.dir names one: normalized, with its slash (`w3c/`),
119# and `` for `.`.
120
121# a package root other than `.`: its directory, the path its SPEC.md is read
122# at (`./w3c/SPEC.md`), and that SPEC.md's text (None when there is none)
123type Package is Data:
124  Package{dir: String, at: String, spec: Maybe<&2, String>}
125
126# the directory of each package
127def roots(ps: List<&2, Package>) -> List<&2, String>:
128  match ps:
129    case Nil{}:
130      Nil{}
131    case Con{Package{dir, _at, _spec}, rest}:
132      dir <> roots(rest)
133
134# whether a directory is among the directories
135def 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
142def 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/`
151def 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 `.`)
155def 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
163def owner(+roots: List<&2, String>, dir: String) -> String:
164  owner.go(ups(dir), roots)
165
166# the files of one package, in order
167def 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# a normalized path relative to a package's directory: every file of a
177# package is under its directory, so this drops the directory
178def rebase.one(pkg: String, norm: String) -> String:
179  String.drop(norm, String.length(pkg))
180
181# a package's files with their normalized paths relative to its directory,
182# as its SPEC.md's Law entries name them
183def rebase(ds: List<&2, Digest.Digest>, +pkg: String) -> List<&2, Digest.Digest>:
184  match ds:
185    case Nil{}:
186      Nil{}
187    case Con{Digest.Digest{path, norm, dir, is_laws, is_law_file, exempt, tops, says, deps, unsafes, laws}, rest}:
188      Digest.Digest{path, rebase.one(pkg, norm), dir, is_laws, is_law_file, exempt, tops, says, deps, unsafes, laws}
189        <> rebase(rest, pkg)
190
191# `trace`'s findings in SPEC.md moved to the package's SPEC.md
192def repath(fs: List<&2, F.Finding>, +at: String) -> List<&2, F.Finding>:
193  match fs:
194    case Nil{}:
195      Nil{}
196    case Con{F.Finding{+path, line, col, len, rule, msg}, rest}:
197      F.Finding{Bool.pick(String, String.eq(path, "SPEC.md"), at, path), line, col, len, rule, msg}
198        <> repath(rest, at)
199
200# `trace` on the package `.`: its files against SPEC.md, and nothing when it
201# holds no file and SPEC.md was not read
202def trace.top(top: List<&2, Digest.Digest>, whole: Bool, spec: Maybe<&2, String>) -> List<&2, F.Finding>:
203  match top:
204    case Con{d, rest}:
205      Trace.check(whole, spec, d <> rest)
206    case Nil{}:
207      match spec:
208        case None{}:
209          Nil{}
210        case Some{text}:
211          Trace.check(whole, Some{text}, [])
212
213# `trace` on another package: its files against its own SPEC.md, the Law
214# entries relative to it and its findings at it; nothing when it has none
215def trace.nested(
216  spec: Maybe<&2, String>,
217  whole: Bool,
218  +pkg: String,
219  +at: String,
220  ds: List<&2, Digest.Digest>
221) -> List<&2, F.Finding>:
222  match spec:
223    case None{}:
224      Nil{}
225    case Some{text}:
226      repath(Trace.check(whole, Some{text}, rebase(ds, pkg)), at)
227
228# `coverage` on each package but `.`
229def covers(ps: List<&2, Package>, +ds: List<&2, Digest.Digest>, +roots: List<&2, String>) -> List<&2, F.Finding>:
230  match ps:
231    case Nil{}:
232      Nil{}
233    case Con{Package{+dir, _at, _spec}, rest}:
234      List.append(&2, F.Finding, Law.check(members(ds, roots, dir)), covers(rest, ds, roots))
235
236# `trace` on each package but `.`
237def traces(
238  ps: List<&2, Package>,
239  +ds: List<&2, Digest.Digest>,
240  +roots: List<&2, String>,
241  +whole: Bool
242) -> List<&2, F.Finding>:
243  match ps:
244    case Nil{}:
245      Nil{}
246    case Con{Package{+dir, +at, spec}, rest}:
247      List.append(&2, F.Finding, trace.nested(spec, whole, dir, at, members(ds, roots, dir)),
248        traces(rest, ds, roots, whole))
249
250# every project rule's findings on the files, SPEC.md's text (None when there
251# is none) and the packages but `.`: each file is read down to its digest once
252# (rules/digest.bend); `coverage` and `trace` run on each package's files,
253# `.` first, and `unsafe` on every file
254def project(
255  srcs: List<&2, Src.Src>,
256  +whole: Bool,
257  spec: Maybe<&2, String>,
258  +ps: List<&2, Package>
259) -> List<&2, F.Finding>:
260  +ds = Digest.all(srcs)
261  +rs = roots(ps)
262  +top = members(ds, rs, "")
263  List.concat(&2, F.Finding, [Law.check(top), covers(ps, ds, rs), Unsafe.check(ds), trace.top(top, whole, spec),
264    traces(ps, ds, rs, whole)])
265
266# the group a rule is in; an unknown slug grades with correctness
267def group(+rule: String) -> String:
268  +named = Codes.group(rule)
269  Bool.pick(String, String.is_empty(named), "correctness", named)
270
271# the findings at the levels a config gives their rules; the ones set off
272# are dropped
273def graded(+cfg: Config.Config, fs: List<&2, F.Finding>) -> List<&2, F.Graded>:
274  match fs:
275    case Nil{}:
276      Nil{}
277    case Con{F.Finding{path, line, col, len, +rule, msg}, rest}:
278      +more = graded(cfg, rest)
279      +l = Config.level(cfg, group(rule), rule)
280      Bool.pick(List<&2, F.Graded>, Config.is_off(l), more,
281        F.Graded{l, F.Finding{path, line, col, len, rule, msg}} <> more)