src/config.bend
bolt@1.20.0.0 · 291 lines · 0 definitions
1# src/config: what a project asks of bolt, read from the `bolt.bend` nearest
2# a file (its own directory, resolved against the working directory by `home`,
3# then each parent up to the root: `chain`, read by the caller, then
4# `nearest`). The file is plain Bend that
5# `bend` can check: a def a setting, its body one string. A group sets every
6# rule in it, a rule set by name wins over its group, and an unset group has
7# its default (correctness at error, pedantic off, the rest at warn):
8#
9# # every rule an error
10# def correctness() -> String:
11# "error"
12# def style() -> String:
13# "error"
14# # but width is advice here
15# def space() -> String:
16# "warn"
17#
18# A level is "off", "warn" or "error". A setting whose name is no rule's slug
19# and no group is `unknown`, and sets nothing.
20import Base
21import bolt@1.20.0.0/src/lazy/lazy.bend as Lazy
22import bolt@1.20.0.0/src/syntax/lex.bend as Lex
23import bolt@1.20.0.0/src/rchars.bend as Rchars
24import bolt@1.20.0.0/src/syntax/tree.bend as Tree
25import bolt@1.20.0.0/src/syntax/bind.bend as Bind
26import bolt@1.20.0.0/src/codes.bend as Codes
27
28# how much a finding matters
29type Level is Data:
30 Off{}
31 Warn{}
32 Error{}
33
34# a name (a group's or a rule's) at a level
35type LevelSet is Data:
36 LevelSet{name: String, level: Level}
37
38# the settings, in file order
39type Config is Data:
40 Config{sets: List<&2, LevelSet>}
41
42# no file: the defaults
43def none() -> Config:
44 Config{[]}
45
46# the word for a level, as a finding is shown
47def show(ll: Level) -> String:
48 match ll:
49 case Off{}:
50 "off"
51 case Warn{}:
52 "warning"
53 case Error{}:
54 "error"
55
56# is it an error?
57def is_error(ll: Level) -> Bool:
58 match ll:
59 case Error{}:
60 True{}
61 case other:
62 False{}
63
64# is it off?
65def is_off(ll: Level) -> Bool:
66 match ll:
67 case Off{}:
68 True{}
69 case other:
70 False{}
71
72# reading the file
73# ----------------
74
75# a level by its word; an unknown word is an error, so a typo shows
76def of_word(+ww: String) -> Level:
77 Bool.pick(Level, String.eq(ww, "off"), Off{}, Bool.pick(Level, String.eq(ww, "warn"), Warn{}, Error{}))
78
79# a string literal's text, its quotes dropped
80def unquote(+tt: String) -> String:
81 Bool.pick(String, Bool.and(String.starts_with(tt, "\""), String.ends_with(tt, "\"")),
82 String.take(String.drop(tt, 1n), (String.length(tt) - 2n : Nat)), tt)
83
84# the first string among the tokens
85def first_str(toks: List<&2, Lex.Tok>) -> Maybe<&2, String>:
86 match toks:
87 case Nil{}:
88 None{}
89 case Con{Lex.Tok{Lex.TStr{}, t, l, c}, rest}:
90 Some{unquote(t)}
91 case Con{h, rest}:
92 first_str(rest)
93
94# a setting as a bolt.bend writes it: the def's name, the word its body
95# returns, and the line of its `def` (0-based)
96type Setting is Data:
97 Setting{name: String, word: String, line: U32}
98
99# a def's name and the string it returns, at its line, as a setting
100def setting(name: Maybe<&2, String>, word: Maybe<&2, String>, +line: U32) -> List<&2, Setting>:
101 match name word:
102 case Some{n} Some{w}:
103 [Setting{n, w, line}]
104 case _a _b:
105 []
106
107# every `def x(): "level"` of a tree
108def settings.go(root: Tree.Node) -> List<&2, Setting>:
109 match root:
110 case Tree.NCons{Tree.Stmt{Tree.SDef{}, +kids, body}, rest}:
111 +ln = Tree.line(kids)
112 List.append(&2, Setting, setting(Bind.declared(kids), first_str(Tree.leaves(body)), ln), settings.go(rest))
113 case Tree.NCons{_h, rest}:
114 settings.go(rest)
115 case _other:
116 Nil{}
117
118# every setting of a bolt.bend's text, in file order
119def settings(text: String) -> List<&2, Setting>:
120 settings.go(Tree.parse(text))
121
122# each setting as the level it sets its name to
123def levels(ss: List<&2, Setting>) -> List<&2, LevelSet>:
124 match ss:
125 case Nil{}:
126 Nil{}
127 case Con{Setting{n, w, _line}, rest}:
128 LevelSet{n, of_word(w)} <> levels(rest)
129
130# a bolt.bend's text as a config: its settings, each at its level
131def parse(text: String) -> Config:
132 Config{levels(settings(text))}
133
134# unknown names
135# -------------
136# A name a bolt.bend may set is a rule's slug or a group, as the code table
137# (src/codes.bend) lists them; a setting of any other name sets nothing, so
138# `setting` (C011) reports it.
139
140# some row of the table has the name as its group
141def known.group(rows: List<&2, Codes.Entry>, +name: String) -> Bool:
142 match rows:
143 case Nil{}:
144 False{}
145 case Con{Codes.Entry{_s, _id, +grp}, rest}:
146 Lazy.or_else(String.eq(grp, name), _u => known.group(rest, name))
147
148# some row of the table has the name as its slug or its group
149def known.in(+rows: List<&2, Codes.Entry>, +name: String) -> Bool:
150 +slug = Maybe.is_some(&2, Codes.Entry, Codes.find(rows, name))
151 +grp = known.group(rows, name)
152 Bool.or(slug, grp)
153
154# the settings, in order, whose name no row of the table has as its slug or
155# its group
156def unknown.in(ss: List<&2, Setting>, +rows: List<&2, Codes.Entry>) -> List<&2, Setting>:
157 match ss:
158 case Nil{}:
159 Nil{}
160 case Con{Setting{+n, w, line}, rest}:
161 +more = unknown.in(rest, rows)
162 Bool.pick(List<&2, Setting>, known.in(rows, n),
163 more, Setting{n, w, line} <> more)
164
165# the settings, in order, whose name is neither a rule's slug nor a group of
166# the code table
167def unknown(ss: List<&2, Setting>) -> List<&2, Setting>:
168 unknown.in(ss, Codes.table())
169
170# a path's directory, with its slash; `` at the top
171def dir_of.go(cs: List<&2, Char>, +acc: List<&2, Char>, best: List<&2, Char>) -> String:
172 match cs:
173 case Nil{}:
174 Rchars.text(best)
175 case Con{'/', t}:
176 dir_of.go(t, '/' <> acc, '/' <> acc)
177 case Con{c, t}:
178 dir_of.go(t, c <> acc, best)
179
180# a path's directory, with its slash; `` at the top
181def dir_of(path: String) -> String:
182 dir_of.go(String.to_list(path), [], [])
183
184# resolving a path
185# ----------------
186# A path is resolved against the working directory before its candidates are
187# taken, so the file's own directory and its real parents are tried wherever
188# bolt runs from: `a.bend` from `/w/p/sub` is graded by `/w/p/sub/bolt.bend`,
189# then `/w/p/bolt.bend`, `/w/bolt.bend` and `/bolt.bend`. `.` and `..` are read
190# lexically, as ruff and `path.resolve` in node read them: a symbolic link is
191# not followed.
192
193# a path from the root: itself when it starts there, otherwise under the
194# working directory
195def absolute(+cwd: String, +path: String) -> String:
196 Bool.pick(String, String.starts_with(path, "/"), path, cwd ++ "/" ++ path)
197
198# the directories above the innermost one; none above the root
199def home.up(above: List<&2, String>) -> List<&2, String>:
200 match above:
201 case Nil{}:
202 Nil{}
203 case Con{_d, rest}:
204 rest
205
206# a path's segment read onto the directories before it, innermost first: an
207# empty segment (from `//` or the `/` a directory ends in) and `.` stay where
208# they are, `..` goes up one (never above the root), and a name goes into it
209def home.step(+seg: String, +above: List<&2, String>) -> List<&2, String>:
210 Bool.pick(List<&2, String>, Bool.or(String.is_empty(seg), String.eq(seg, ".")), above,
211 Bool.pick(List<&2, String>, String.eq(seg, ".."), home.up(above), seg <> above))
212
213# the directories a path names, innermost first: its segments, split at `/`,
214# each read by `home.step` onto those before it (seg holds the segment being
215# read, reversed)
216def home.segs(cs: List<&2, Char>, seg: List<&2, Char>, above: List<&2, String>) -> List<&2, String>:
217 match cs:
218 case Nil{}:
219 home.step(Rchars.text(seg), above)
220 case Con{'/', t}:
221 home.segs(t, [], home.step(Rchars.text(seg), above))
222 case Con{c, t}:
223 home.segs(t, c <> seg, above)
224
225# a directory resolved against the working directory, as the names of the
226# directories from the root to it, innermost first: `sub/../p3/` under
227# `/w/p1` is `[p3, w]`, and `` is the working directory itself
228def home(cwd: String, dir: String) -> List<&2, String>:
229 home.segs(String.to_list(absolute(cwd, dir)), [], [])
230
231# directories, innermost first, as a path from the root ending in `/`
232def root(ds: List<&2, String>, acc: String) -> String:
233 match ds:
234 case Nil{}:
235 "/" ++ acc
236 case Con{d, rest}:
237 root(rest, d ++ "/" ++ acc)
238
239# the bolt.bend of a resolved directory (innermost first, as `home` gives it)
240# and of each parent up to the root, nearest first: `[p3, w]` is
241# `/w/p3/bolt.bend`, `/w/bolt.bend`, `/bolt.bend`
242def chain(ds: List<&2, String>) -> List<&2, String>:
243 match ds:
244 case Nil{}:
245 ["/bolt.bend"]
246 case Con{d, +rest}:
247 (root(d <> rest, "") ++ "bolt.bend") <> chain(rest)
248
249# the first file that was there, parsed; none, the defaults
250def nearest(rs: List<&2, Maybe<&2, String>>) -> Config:
251 match rs:
252 case Nil{}:
253 none()
254 case Con{Some{text}, rest}:
255 parse(text)
256 case Con{None{}, rest}:
257 nearest(rest)
258
259# levels
260# ------
261
262# the level a name is set to, when it is
263def set_level(sets: List<&2, LevelSet>, +name: String) -> Maybe<&2, Level>:
264 match sets:
265 case Nil{}:
266 None{}
267 case Con{LevelSet{+n, l}, rest}:
268 +more = set_level(rest, name)
269 Bool.pick(Maybe<&2, Level>, String.eq(n, name), Some{l}, more)
270
271# a group's level when nothing sets it
272def default(+group: String) -> Level:
273 Bool.pick(Level, String.eq(group, "correctness"), Error{},
274 Bool.pick(Level, String.eq(group, "pedantic"), Off{}, Warn{}))
275
276# a rule only its own setting turns on: a group setting, or the group's
277# default, would put new findings on projects that never asked for it
278# (`trace`, `thunk`, `scan` and `transcong`)
279def opt_in(+rule: String) -> Bool:
280 List.contains(~String, ~String.eq, ["trace", "thunk", "scan", "transcong"], rule)
281
282# the rule's own setting, else its group's, else the group's default; an
283# opt-in rule's own setting, else off
284def level.of(+sets: List<&2, LevelSet>, +group: String, +rule: String) -> Level:
285 Maybe.default(&2, Level, set_level(sets, rule),
286 Lazy.stop(Level, opt_in(rule), Off{}, _u => Maybe.default(&2, Level, set_level(sets, group), default(group))))
287
288# the rule's own setting, else its group's, else the group's default
289def level(cfg: Config, group: String, rule: String) -> Level:
290 Config{ss} = cfg
291 level.of(ss, group, rule)