src/config.bend

bolt@1.23.0.0 · 299 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.23.0.0/src/lazy/lazy.bend as Lazy
22import bolt@1.23.0.0/src/syntax/lex.bend as Lex
23import bolt@1.23.0.0/src/rchars.bend as Rchars
24import bolt@1.23.0.0/src/syntax/tree.bend as Tree
25import bolt@1.23.0.0/src/syntax/bind.bend as Bind
26import bolt@1.23.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; hit: the setting before sets
263# named it, so the search stops there
264def set_level.go(sets: List<&2, LevelSet>, +name: String, hit: Maybe<&2, Level>) -> Maybe<&2, Level>:
265  match sets:
266    case Nil{}:
267      hit
268    case Con{LevelSet{+n, l}, rest}:
269      match hit:
270        case Some{x}:
271          Some{x}
272        case None{}:
273          set_level.go(rest, name, Bool.pick(Maybe<&2, Level>, String.eq(n, name), Some{l}, None{}))
274
275# the level a name is set to, when it is: the first setting that names it
276def set_level(sets: List<&2, LevelSet>, +name: String) -> Maybe<&2, Level>:
277  set_level.go(sets, name, None{})
278
279# a group's level when nothing sets it
280def default(+group: String) -> Level:
281  Bool.pick(Level, String.eq(group, "correctness"), Error{},
282    Bool.pick(Level, String.eq(group, "pedantic"), Off{}, Warn{}))
283
284# a rule only its own setting turns on: a group setting, or the group's
285# default, would put new findings on projects that never asked for it
286# (`thunk`, `scan` and `transcong`)
287def opt_in(+rule: String) -> Bool:
288  List.contains(~String, ~String.eq, ["thunk", "scan", "transcong"], rule)
289
290# the rule's own setting, else its group's, else the group's default; an
291# opt-in rule's own setting, else off
292def level.of(+sets: List<&2, LevelSet>, +group: String, +rule: String) -> Level:
293  Maybe.default(&2, Level, set_level(sets, rule),
294    Lazy.stop(Level, opt_in(rule), Off{}, _u => Maybe.default(&2, Level, set_level(sets, group), default(group))))
295
296# the rule's own setting, else its group's, else the group's default
297def level(cfg: Config, group: String, rule: String) -> Level:
298  Config{ss} = cfg
299  level.of(ss, group, rule)