src/config.bend

bolt@1.12.0.0 · 233 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".
19import Base
20import bolt@1.12.0.0/src/lazy/lazy.bend as Lazy
21import bolt@1.12.0.0/src/syntax/lex.bend as Lex
22import bolt@1.12.0.0/src/syntax/tree.bend as Tree
23import bolt@1.12.0.0/src/syntax/bind.bend as Bind
24
25# how much a finding matters
26type Level is Data:
27  Off{}
28  Warn{}
29  Error{}
30
31# a name (a group's or a rule's) at a level
32type LevelSet is Data:
33  LevelSet{name: String, level: Level}
34
35# the settings, in file order
36type Config is Data:
37  Config{sets: List<&2, LevelSet>}
38
39# no file: the defaults
40def none() -> Config:
41  Config{[]}
42
43# the word for a level, as a finding is shown
44def show(ll: Level) -> String:
45  match ll:
46    case Off{}:
47      "off"
48    case Warn{}:
49      "warning"
50    case Error{}:
51      "error"
52
53# is it an error?
54def is_error(ll: Level) -> Bool:
55  match ll:
56    case Error{}:
57      True{}
58    case other:
59      False{}
60
61# is it off?
62def is_off(ll: Level) -> Bool:
63  match ll:
64    case Off{}:
65      True{}
66    case other:
67      False{}
68
69# reading the file
70# ----------------
71
72# a level by its word; an unknown word is an error, so a typo shows
73def of_word(+ww: String) -> Level:
74  Bool.pick(Level, String.eq(ww, "off"), Off{}, Bool.pick(Level, String.eq(ww, "warn"), Warn{}, Error{}))
75
76# a string literal's text, its quotes dropped
77def unquote(+tt: String) -> String:
78  Bool.pick(String, Bool.and(String.starts_with(tt, "\""), String.ends_with(tt, "\"")),
79    String.take(String.drop(tt, 1n), (String.length(tt) - 2n : Nat)), tt)
80
81# the first string among the tokens
82def first_str(toks: List<&2, Lex.Tok>) -> Maybe<&2, String>:
83  match toks:
84    case Nil{}:
85      None{}
86    case Con{Lex.Tok{Lex.TStr{}, t, l, c}, rest}:
87      Some{unquote(t)}
88    case Con{h, rest}:
89      first_str(rest)
90
91# a def's name and the string it returns, as a setting
92def setting(name: Maybe<&2, String>, word: Maybe<&2, String>) -> List<&2, LevelSet>:
93  match name word:
94    case Some{n} Some{w}:
95      [LevelSet{n, of_word(w)}]
96    case a b:
97      []
98
99# every `def x(): "level"` of a tree
100def sets(root: Tree.Node) -> List<&2, LevelSet>:
101  match root:
102    case Tree.NCons{Tree.Stmt{Tree.SDef{}, kids, body}, rest}:
103      List.append(&2, LevelSet, setting(Bind.declared(kids), first_str(Tree.leaves(body))), sets(rest))
104    case Tree.NCons{h, rest}:
105      sets(rest)
106    case other:
107      Nil{}
108
109# a bolt.bend's text as a config
110def parse(text: String) -> Config:
111  Config{sets(Tree.parse(text))}
112
113# a path's directory, with its slash; `` at the top
114def dir_of.go(cs: List<&2, Char>, +acc: List<&2, Char>, best: List<&2, Char>) -> String:
115  match cs:
116    case Nil{}:
117      String.from_list(List.reverse(&2, Char, best))
118    case Con{'/', t}:
119      dir_of.go(t, '/' <> acc, '/' <> acc)
120    case Con{c, t}:
121      dir_of.go(t, c <> acc, best)
122
123# a path's directory, with its slash; `` at the top
124def dir_of(path: String) -> String:
125  dir_of.go(String.to_list(path), [], [])
126
127# resolving a path
128# ----------------
129# A path is resolved against the working directory before its candidates are
130# taken, so the file's own directory and its real parents are tried wherever
131# bolt runs from: `a.bend` from `/w/p/sub` is graded by `/w/p/sub/bolt.bend`,
132# then `/w/p/bolt.bend`, `/w/bolt.bend` and `/bolt.bend`. `.` and `..` are read
133# lexically, as ruff and `path.resolve` in node read them: a symbolic link is
134# not followed.
135
136# a path from the root: itself when it starts there, otherwise under the
137# working directory
138def absolute(+cwd: String, +path: String) -> String:
139  Bool.pick(String, String.starts_with(path, "/"), path, cwd ++ "/" ++ path)
140
141# the directories above the innermost one; none above the root
142def home.up(above: List<&2, String>) -> List<&2, String>:
143  match above:
144    case Nil{}:
145      Nil{}
146    case Con{_d, rest}:
147      rest
148
149# a path's segment read onto the directories before it, innermost first: an
150# empty segment (from `//` or the `/` a directory ends in) and `.` stay where
151# they are, `..` goes up one (never above the root), and a name goes into it
152def home.step(+seg: String, +above: List<&2, String>) -> List<&2, String>:
153  Bool.pick(List<&2, String>, Bool.or(String.is_empty(seg), String.eq(seg, ".")), above,
154    Bool.pick(List<&2, String>, String.eq(seg, ".."), home.up(above), seg <> above))
155
156# the directories a path names, innermost first: its segments, split at `/`,
157# each read by `home.step` onto those before it (seg holds the segment being
158# read, reversed)
159def home.segs(cs: List<&2, Char>, seg: List<&2, Char>, above: List<&2, String>) -> List<&2, String>:
160  match cs:
161    case Nil{}:
162      home.step(String.from_list(List.reverse(&2, Char, seg)), above)
163    case Con{'/', t}:
164      home.segs(t, [], home.step(String.from_list(List.reverse(&2, Char, seg)), above))
165    case Con{c, t}:
166      home.segs(t, c <> seg, above)
167
168# a directory resolved against the working directory, as the names of the
169# directories from the root to it, innermost first: `sub/../p3/` under
170# `/w/p1` is `[p3, w]`, and `` is the working directory itself
171def home(cwd: String, dir: String) -> List<&2, String>:
172  home.segs(String.to_list(absolute(cwd, dir)), [], [])
173
174# directories, innermost first, as a path from the root ending in `/`
175def root(ds: List<&2, String>, acc: String) -> String:
176  match ds:
177    case Nil{}:
178      "/" ++ acc
179    case Con{d, rest}:
180      root(rest, d ++ "/" ++ acc)
181
182# the bolt.bend of a resolved directory (innermost first, as `home` gives it)
183# and of each parent up to the root, nearest first: `[p3, w]` is
184# `/w/p3/bolt.bend`, `/w/bolt.bend`, `/bolt.bend`
185def chain(ds: List<&2, String>) -> List<&2, String>:
186  match ds:
187    case Nil{}:
188      ["/bolt.bend"]
189    case Con{d, +rest}:
190      (root(d <> rest, "") ++ "bolt.bend") <> chain(rest)
191
192# the first file that was there, parsed; none, the defaults
193def nearest(rs: List<&2, Maybe<&2, String>>) -> Config:
194  match rs:
195    case Nil{}:
196      none()
197    case Con{Some{text}, rest}:
198      parse(text)
199    case Con{None{}, rest}:
200      nearest(rest)
201
202# levels
203# ------
204
205# the level a name is set to, when it is
206def set_level(sets: List<&2, LevelSet>, +name: String) -> Maybe<&2, Level>:
207  match sets:
208    case Nil{}:
209      None{}
210    case Con{LevelSet{+n, l}, rest}:
211      +more = set_level(rest, name)
212      Bool.pick(Maybe<&2, Level>, String.eq(n, name), Some{l}, more)
213
214# a group's level when nothing sets it
215def default(+group: String) -> Level:
216  Bool.pick(Level, String.eq(group, "correctness"), Error{},
217    Bool.pick(Level, String.eq(group, "pedantic"), Off{}, Warn{}))
218
219# a rule only its own setting turns on: a group setting, or the group's
220# default, would put new findings on projects that never asked for it
221def opt_in(+rule: String) -> Bool:
222  String.eq(rule, "trace")
223
224# the rule's own setting, else its group's, else the group's default; an
225# opt-in rule's own setting, else off
226def level.of(+sets: List<&2, LevelSet>, +group: String, +rule: String) -> Level:
227  Maybe.default(&2, Level, set_level(sets, rule),
228    Lazy.stop(Level, opt_in(rule), Off{}, _u => Maybe.default(&2, Level, set_level(sets, group), default(group))))
229
230# the rule's own setting, else its group's, else the group's default
231def level(cfg: Config, group: String, rule: String) -> Level:
232  Config{ss} = cfg
233  level.of(ss, group, rule)