src/lint/plan.bend
bolt@1.27.0.0 · 637 lines · 77 definitions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import Base
22import bolt@1.27.0.0/src/lazy/lazy.bend as Lazy
23import bolt@1.27.0.0/src/lint/world.bend as W
24import bolt@1.27.0.0/src/finding.bend as F
25import bolt@1.27.0.0/src/src.bend as Src
26import bolt@1.27.0.0/src/role.bend as Role
27import bolt@1.27.0.0/src/rules.bend as Rules
28import bolt@1.27.0.0/src/rules/imports.bend as Imports
29import bolt@1.27.0.0/src/config.bend as Config
30import bolt@1.27.0.0/src/noqa.bend as Noqa
31import bolt@1.27.0.0/src/rules/style/noqa.bend as NoqaRule
32import bolt@1.27.0.0/src/rules/correctness/setting.bend as Setting
33
34
35type Plan is Data:
36 Plan{lines: List<&2, String>, exit: U32}
37
38
39def lines.of(pl: Plan) -> List<&2, String>:
40 Plan{lines, _exit} = pl
41 lines
42
43
44def exit.of(pl: Plan) -> U32:
45 Plan{_lines, exit} = pl
46 exit
47
48
49
50
51
52
53
54
55
56def walk.skipped(+name: String) -> Bool:
57 Bool.or(String.starts_with(name, "."), String.eq(name, "node_modules"))
58
59
60def walk.bare(+name: String) -> String:
61 String.take(name, Nat.sub(String.length(name), 1n))
62
63
64def walk.under(+dir: String, +name: String) -> String:
65 dir ++ "/" ++ name
66
67
68def walk.dirs(es: List<&2, String>, +dir: String) -> List<&2, String>:
69 match es:
70 case Nil{}:
71 Nil{}
72 case Con{+e, rest}:
73 +more = walk.dirs(rest, dir)
74 +keep = Bool.and(String.ends_with(e, "/"), Bool.not(walk.skipped(walk.bare(e))))
75 Bool.pick(List<&2, String>, keep, walk.under(dir, walk.bare(e)) <> more, more)
76
77
78def walk.bends(es: List<&2, String>, +dir: String) -> List<&2, String>:
79 match es:
80 case Nil{}:
81 Nil{}
82 case Con{+e, rest}:
83 +more = walk.bends(rest, dir)
84 Bool.pick(List<&2, String>, String.ends_with(e, ".bend"), walk.under(dir, e) <> more, more)
85
86
87
88type Walk is Data:
89 Walk{found: List<&2, String>, read: List<&2, String>}
90
91
92def walk.found(wk: Walk) -> List<&2, String>:
93 Walk{found, _read} = wk
94 found
95
96
97def walk.read(wk: Walk) -> List<&2, String>:
98 Walk{_found, read} = wk
99 List.reverse(&2, String, read)
100
101
102
103
104
105def walk(
106 fuel: Nat,
107 +world: W.World,
108 queue: List<&2, String>,
109 acc: List<&2, String>,
110 seen: List<&2, String>
111) -> Walk:
112 match fuel:
113 case 0n:
114 Walk{acc, seen}
115 case 1n+f:
116 match queue:
117 case Nil{}:
118 Walk{acc, seen}
119 case Con{+dir, rest}:
120 +es = W.listing(world, dir)
121 walk(f, world, List.append(&2, String, rest, walk.dirs(es, dir)),
122 List.append(&2, String, walk.bends(es, dir), acc), dir <> seen)
123
124
125
126
127
128
129def limit() -> Nat:
130 100000n
131
132
133def tree(+world: W.World) -> Walk:
134 walk(limit(), world, ["."], [], [])
135
136
137
138
139
140def files.named(paths: List<&2, String>, +world: W.World) -> List<&2, String>:
141 match paths:
142 case Nil{}:
143 +found = walk.found(tree(world))
144 List.sort(~String, ~String.is_le, found)
145 case Con{p, rest}:
146 p <> rest
147
148
149def files(+world: W.World) -> List<&2, String>:
150 files.named(W.paths.of(world), world)
151
152
153def is_config(+pp: String) -> Bool:
154 Bool.or(String.eq(pp, "bolt.bend"), String.ends_with(pp, "/bolt.bend"))
155
156
157def sources(ps: List<&2, String>) -> List<&2, String>:
158 match ps:
159 case Nil{}:
160 Nil{}
161 case Con{+p, rest}:
162 +more = sources(rest)
163 Bool.pick(List<&2, String>, is_config(p), more, p <> more)
164
165
166def inputs(+world: W.World) -> List<&2, String>:
167 sources(files(world))
168
169
170
171
172
173def abs(+cwd: String, +path: String) -> String:
174 Imports.norm(Bool.pick(String, String.starts_with(path, "/"), path, cwd ++ "/" ++ path))
175
176
177def norms(ps: List<&2, String>, +cwd: String) -> List<&2, String>:
178 match ps:
179 case Nil{}:
180 Nil{}
181 case Con{p, rest}:
182 abs(cwd, p) <> norms(rest, cwd)
183
184
185def unseen(ps: List<&2, String>, +cwd: String, +seen: List<&2, String>) -> List<&2, String>:
186 match ps:
187 case Nil{}:
188 Nil{}
189 case Con{+p, rest}:
190 +more = unseen(rest, cwd, seen)
191 Bool.pick(List<&2, String>, Imports.has(seen, abs(cwd, p)), more, p <> more)
192
193
194
195
196
197
198def graph(+world: W.World) -> List<&2, String>:
199 +found = sources(walk.found(tree(world)))
200 +cwd = W.cwd.of(world)
201 List.append(&2, String, found, unseen(inputs(world), cwd, norms(found, cwd)))
202
203
204
205
206
207def seen.has(ps: List<&2, String>, +pp: String) -> Bool:
208 match ps:
209 case Nil{}:
210 False{}
211 case Con{p, rest}:
212 Lazy.or_else(String.eq(p, pp), _u => seen.has(rest, pp))
213
214
215def uniq(ps: List<&2, String>, +seen: List<&2, String>) -> List<&2, String>:
216 match ps:
217 case Nil{}:
218 Nil{}
219 case Con{+p, rest}:
220 +dup = seen.has(seen, p)
221 +more = uniq(rest, p <> seen)
222 Bool.pick(List<&2, String>, dup, more, p <> more)
223
224
225
226
227
228
229
230def roots.of(ds: List<&2, String>, +world: W.World) -> List<&2, String>:
231 match ds:
232 case Nil{}:
233 Nil{}
234 case Con{+d, rest}:
235 +more = roots.of(rest, world)
236 +root = Bool.and(Bool.not(String.eq(d, ".")), seen.has(W.listing(world, d), "main.bend"))
237 Bool.pick(List<&2, String>, root, d <> more, more)
238
239
240def roots.named(paths: List<&2, String>, +world: W.World) -> List<&2, String>:
241 match paths:
242 case Nil{}:
243 roots.of(walk.read(tree(world)), world)
244 case Con{_p, _rest}:
245 Nil{}
246
247
248def roots(+world: W.World) -> List<&2, String>:
249 roots.named(W.paths.of(world), world)
250
251
252
253def packages(rs: List<&2, String>) -> List<&2, String>:
254 match rs:
255 case Nil{}:
256 Nil{}
257 case Con{r, rest}:
258 Imports.dir_of(r ++ "/main.bend") <> packages(rest)
259
260
261def dirs.of(ps: List<&2, String>) -> List<&2, String>:
262 match ps:
263 case Nil{}:
264 Nil{}
265 case Con{p, rest}:
266 Config.dir_of(p) <> dirs.of(rest)
267
268
269
270def conf.dirs(+ps: List<&2, String>) -> List<&2, String>:
271 uniq(dirs.of(ps), [])
272
273
274
275
276def conf.chain(+world: W.World, dir: String) -> List<&2, String>:
277 Config.chain(Config.home(W.cwd.of(world), dir))
278
279
280def conf.cands(ds: List<&2, String>, +world: W.World) -> List<&2, String>:
281 match ds:
282 case Nil{}:
283 Nil{}
284 case Con{d, rest}:
285 List.append(&2, String, conf.chain(world, d), conf.cands(rest, world))
286
287
288def asks.entries(ds: List<&2, String>) -> List<&2, W.Ask>:
289 match ds:
290 case Nil{}:
291 Nil{}
292 case Con{d, rest}:
293 W.Entries{d} <> asks.entries(rest)
294
295
296def asks.texts(ps: List<&2, String>) -> List<&2, W.Ask>:
297 match ps:
298 case Nil{}:
299 Nil{}
300 case Con{p, rest}:
301 W.Contents{p} <> asks.texts(rest)
302
303
304
305
306def lists(+world: W.World) -> List<&2, W.Ask>:
307 asks.entries(walk.read(tree(world)))
308
309
310
311def texts(+world: W.World) -> List<&2, W.Ask>:
312 +ps = inputs(world)
313 asks.texts(uniq(List.concat(&2, String, [ps, graph(world), conf.cands(conf.dirs(ps), world)]),
314 []))
315
316
317
318def looks(+world: W.World) -> List<&2, W.Ask>:
319 List.append(&2, W.Ask, lists(world), texts(world))
320
321
322def unanswered(asks: List<&2, W.Ask>, +world: W.World) -> List<&2, W.Ask>:
323 match asks:
324 case Nil{}:
325 Nil{}
326 case Con{+a, rest}:
327 +more = unanswered(rest, world)
328 Bool.pick(List<&2, W.Ask>, W.answered(world, a), more, a <> more)
329
330
331def wants.pick(ls: List<&2, W.Ask>, +world: W.World) -> List<&2, W.Ask>:
332 match ls:
333 case Nil{}:
334 unanswered(texts(world), world)
335 case Con{a, rest}:
336 a <> rest
337
338
339
340def wants(+world: W.World) -> List<&2, W.Ask>:
341 wants.pick(unanswered(lists(world), world), world)
342
343
344
345
346
347type Got is Data:
348 Got{path: String, text: Maybe<&2, String>}
349
350
351def gots(ps: List<&2, String>, +world: W.World) -> List<&2, Got>:
352 match ps:
353 case Nil{}:
354 Nil{}
355 case Con{+p, rest}:
356 Got{p, W.text(world, p)} <> gots(rest, world)
357
358
359def unread(gs: List<&2, Got>) -> List<&2, F.Finding>:
360 match gs:
361 case Nil{}:
362 Nil{}
363 case Con{Got{+path, None{}}, rest}:
364 F.Finding{path, 0, 0, 0, "read", "Cannot read this file."} <> unread(rest)
365 case Con{Got{_path, Some{_text}}, rest}:
366 unread(rest)
367
368
369def srcs_of(gs: List<&2, Got>) -> List<&2, Src.Src>:
370 match gs:
371 case Nil{}:
372 Nil{}
373 case Con{Got{_path, None{}}, rest}:
374 srcs_of(rest)
375 case Con{Got{path, Some{text}}, rest}:
376 Src.of(path, text) <> srcs_of(rest)
377
378
379
380def per_file.role(+ms: List<&2, Role.Mark>, ss: List<&2, Src.Src>) -> List<&2, F.Finding>:
381 match ss:
382 case Nil{}:
383 Nil{}
384 case Con{s, rest}:
385 Src.Src{+path, text, toks, tree, bound, items} = s
386 List.concat(&2, F.Finding, [Rules.on.role(Role.of(ms, path), Src.Src{path, text, toks, tree, bound, items}),
387 per_file.role(ms, rest)])
388
389
390
391def facts.of(gs: List<&2, Got>, +cwd: String) -> List<&2, Role.Fact>:
392 match gs:
393 case Nil{}:
394 Nil{}
395 case Con{Got{_path, None{}}, rest}:
396 facts.of(rest, cwd)
397 case Con{Got{path, Some{text}}, rest}:
398 Role.fact.text(abs(cwd, path), text) <> facts.of(rest, cwd)
399
400
401
402def marks.key(ps: List<&2, String>, +cwd: String, +ms: List<&2, Role.Mark>) -> List<&2, Role.Mark>:
403 match ps:
404 case Nil{}:
405 Nil{}
406 case Con{+p, rest}:
407 Role.Mark{Imports.norm(p), Role.of.norm(ms, abs(cwd, p))} <> marks.key(rest, cwd, ms)
408
409
410
411
412def marks.named(+world: W.World) -> List<&2, Role.Mark>:
413 +cwd = W.cwd.of(world)
414 marks.key(inputs(world), cwd, Role.marks.facts(facts.of(gots(graph(world), world), cwd)))
415
416
417
418
419
420def marks.run(paths: List<&2, String>, +world: W.World, +ss: List<&2, Src.Src>) -> List<&2, Role.Mark>:
421 match paths:
422 case Nil{}:
423 Role.marks(ss)
424 case Con{_p, _rest}:
425 marks.named(world)
426
427
428
429def conf.first(gs: List<&2, Got>) -> List<&2, String>:
430 match gs:
431 case Nil{}:
432 Nil{}
433 case Con{Got{path, Some{_text}}, _rest}:
434 [path]
435 case Con{Got{_path, None{}}, rest}:
436 conf.first(rest)
437
438
439
440
441def conf.used(ds: List<&2, String>, +world: W.World) -> List<&2, String>:
442 match ds:
443 case Nil{}:
444 Nil{}
445 case Con{d, rest}:
446 List.append(&2, String, conf.first(gots(conf.chain(world, d), world)), conf.used(rest, world))
447
448
449def graders(+world: W.World) -> List<&2, String>:
450 uniq(conf.used(conf.dirs(inputs(world)), world), [])
451
452
453def settings.at(gs: List<&2, Got>) -> List<&2, F.Finding>:
454 match gs:
455 case Nil{}:
456 Nil{}
457 case Con{Got{+path, Some{text}}, rest}:
458 List.append(&2, F.Finding, Setting.check(path, text), settings.at(rest))
459 case Con{Got{_path, None{}}, rest}:
460 settings.at(rest)
461
462
463
464def settings(+world: W.World) -> List<&2, F.Finding>:
465 settings.at(gots(graders(world), world))
466
467
468
469
470
471def found.at(+world: W.World, +gs: List<&2, Got>, +ss: List<&2, Src.Src>) -> List<&2, F.Finding>:
472 List.concat(&2, F.Finding, [unread(gs), per_file.role(marks.run(W.paths.of(world), world, ss), ss), settings(world),
473 Rules.project(ss, packages(roots(world)))])
474
475
476def found(+world: W.World) -> List<&2, F.Finding>:
477 +gs = gots(inputs(world), world)
478 found.at(world, gs, srcs_of(gs))
479
480
481
482
483
484type Conf is Data:
485 Conf{dir: String, config: Config.Config}
486
487
488def texts.at(ps: List<&2, String>, +world: W.World) -> List<&2, Maybe<&2, String>>:
489 match ps:
490 case Nil{}:
491 Nil{}
492 case Con{p, rest}:
493 W.text(world, p) <> texts.at(rest, world)
494
495
496
497def conf.near(+world: W.World, dir: String) -> Config.Config:
498 Config.nearest(texts.at(conf.chain(world, dir), world))
499
500
501def confs.at(ds: List<&2, String>, +world: W.World) -> List<&2, Conf>:
502 match ds:
503 case Nil{}:
504 Nil{}
505 case Con{+d, rest}:
506 Conf{d, conf.near(world, d)} <> confs.at(rest, world)
507
508
509def confs(+world: W.World) -> List<&2, Conf>:
510 confs.at(conf.dirs(inputs(world)), world)
511
512
513
514
515def conf.find(cs: List<&2, Conf>, +world: W.World, +dir: String) -> Config.Config:
516 match cs:
517 case Nil{}:
518 conf.near(world, dir)
519 case Con{Conf{d, cfg}, rest}:
520 Lazy.stop(Config.Config, String.eq(d, dir), cfg, _u => conf.find(rest, world, dir))
521
522
523def config.at(+cs: List<&2, Conf>, +world: W.World, path: String) -> Config.Config:
524 conf.find(cs, world, Config.dir_of(path))
525
526
527
528def grade(+cs: List<&2, Conf>, +world: W.World, fs: List<&2, F.Finding>) -> List<&2, F.Graded>:
529 match fs:
530 case Nil{}:
531 Nil{}
532 case Con{F.Finding{+path, line, col, len, rule, msg}, rest}:
533 cfg = config.at(cs, world, path)
534 List.append(&2, F.Graded, Rules.graded(cfg, [F.Finding{path, line, col, len, rule, msg}]), grade(cs, world, rest))
535
536
537
538
539
540
541def noqa(ms: List<&2, Noqa.Marks>, +whole: Bool, +gs: List<&2, F.Graded>) -> List<&2, F.Finding>:
542 match ms:
543 case Nil{}:
544 Nil{}
545 case Con{Noqa.Marks{+path, mks}, rest}:
546 List.append(&2, F.Finding, Rules.on.at(path, NoqaRule.check(mks, whole, path, gs)), noqa(rest, whole, gs))
547
548
549
550def after(
551 +ms: List<&2, Noqa.Marks>,
552 +whole: Bool,
553 +gs: List<&2, F.Graded>,
554 grader: List<&2, F.Finding> -> List<&2, F.Graded>
555) -> List<&2, F.Graded>:
556 List.append(&2, F.Graded, Noqa.keep(ms, gs), grader(noqa(ms, whole, gs)))
557
558
559
560
561def final(+world: W.World) -> List<&2, F.Graded>:
562 +gs = gots(inputs(world), world)
563 +ss = srcs_of(gs)
564 +cs = confs(world)
565 after(Noqa.of_srcs(ss), List.is_empty(&2, String, W.paths.of(world)), grade(cs, world, found.at(world, gs, ss)),
566 fs => grade(cs, world, fs))
567
568
569
570
571
572def errors.at(ll: Config.Level, more: U32) -> U32:
573 match ll:
574 case Config.Off{}:
575 more
576 case Config.Warn{}:
577 more
578 case Config.Error{}:
579 U32.inc(more)
580
581
582def errors(gs: List<&2, F.Graded>) -> U32:
583 match gs:
584 case Nil{}:
585 0
586 case Con{F.Graded{ll, _ff}, rest}:
587 errors.at(ll, errors(rest))
588
589
590def warnings.at(ll: Config.Level, more: U32) -> U32:
591 match ll:
592 case Config.Off{}:
593 U32.inc(more)
594 case Config.Warn{}:
595 U32.inc(more)
596 case Config.Error{}:
597 more
598
599
600def warnings(gs: List<&2, F.Graded>) -> U32:
601 match gs:
602 case Nil{}:
603 0
604 case Con{F.Graded{ll, _ff}, rest}:
605 warnings.at(ll, warnings(rest))
606
607
608def has_error(gs: List<&2, F.Graded>) -> Bool:
609 match gs:
610 case Nil{}:
611 False{}
612 case Con{F.Graded{ll, _ff}, rest}:
613 +more = has_error(rest)
614 Bool.or(Config.is_error(ll), more)
615
616
617def count(+nn: U32, +what: String) -> String:
618 U32.show(nn) ++ " " ++ what ++ Bool.pick(String, U32.is_eq(nn, 1), "", "s")
619
620
621def summary(+gs: List<&2, F.Graded>) -> String:
622 Lazy.stop(String, List.is_empty(&2, F.Graded, gs), "clean",
623 _u => count(errors(gs), "error") ++ ", " ++ count(warnings(gs), "warning"))
624
625
626def show_all(gs: List<&2, F.Graded>) -> List<&2, String>:
627 match gs:
628 case Nil{}:
629 Nil{}
630 case Con{g, rest}:
631 F.show_graded(g) <> show_all(rest)
632
633
634
635def plan(+world: W.World) -> Plan:
636 +gs = final(world)
637 Plan{List.append(&2, String, show_all(gs), [summary(gs)]), Bool.pick(U32, has_error(gs), 1, 0)}