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. 20 import Base 21 import ./lazy/lazy.bend as Lazy 22 import ./syntax/lex.bend as Lex 23 import ./rchars.bend as Rchars 24 import ./syntax/tree.bend as Tree 25 import ./syntax/bind.bend as Bind 26 import ./codes.bend as Codes 27 28 # how much a finding matters 29 type Level is Data: 30 Off{} 31 Warn{} 32 Error{} 33 34 # a name (a group's or a rule's) at a level 35 type LevelSet is Data: 36 LevelSet{name: String, level: Level} 37 38 # the settings, in file order 39 type Config is Data: 40 Config{sets: List<&2, LevelSet>} 41 42 # no file: the defaults 43 def none() -> Config: 44 Config{[]} 45 46 # the word for a level, as a finding is shown 47 def 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? 57 def is_error(ll: Level) -> Bool: 58 match ll: 59 case Error{}: 60 True{} 61 case other: 62 False{} 63 64 # is it off? 65 def 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 76 def 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 80 def 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 85 def 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) 96 type 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 100 def 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 108 def 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 119 def settings(text: String) -> List<&2, Setting>: 120 settings.go(Tree.parse(text)) 121 122 # each setting as the level it sets its name to 123 def 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 131 def 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 141 def 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 149 def 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 156 def 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 167 def 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 171 def 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 181 def 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 195 def 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 199 def 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 209 def 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) 216 def 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 228 def 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 `/` 232 def 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` 242 def 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 250 def 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 263 def 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 272 def 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` and `scan`) 279 def opt_in(+rule: String) -> Bool: 280 List.contains(~String, ~String.eq, ["trace", "thunk", "scan"], 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 284 def 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 289 def level(cfg: Config, group: String, rule: String) -> Level: 290 Config{ss} = cfg 291 level.of(ss, group, rule)