lib/linter/directives.bend
bend-lint-rules@0.2.0.0 · 1,111 lines · 101 definitions
1import Base
2import bend-lint-api@0.2.0.0/shared/lists.bend as Lists
3import bend-lint-api@0.2.0.0/shared/names.bend as Names
4import bend-lint-api@0.2.0.0/shared/strings.bend as Strings
5import bend-lint-rules@0.2.0.0/lib/text/main.bend as Text
6import bend-lint-api@0.2.0.0/linter/lint.bend as Lint
7
8
9
10
11
12
13type Comment is Data:
14 Comment{beg: U32, end: U32, line: U32, trailing: Bool, body: String}
15
16type Glyph is Data:
17 LineEnd{}
18 CarriageReturn{}
19 Hash{}
20 DoubleQuote{}
21 SingleQuote{}
22 Backslash{}
23 Blank{}
24 Other{}
25
26
27type Mode is Data:
28 InCode{}
29 InComment{}
30 InQuote{string: Bool, escaped: Bool}
31
32
33
34type Scan is Data:
35 Scan{
36 mode: Mode,
37 at: U32,
38 line: U32,
39 code: Bool,
40 open: Comment,
41 comments: List<&2, Comment>,
42 lines: List<&2, Bool>
43 }
44
45type Scanned is Data:
46 Scanned{comments: List<&2, Comment>, lines: List<&2, Bool>}
47
48def Glyph.of(+c: Char) -> Glyph:
49 Bool.pick(
50 Glyph,
51 Char.is_eq(c, '\n'),
52 LineEnd{},
53 Bool.pick(
54 Glyph,
55 Char.is_eq(c, '#'),
56 Hash{},
57 Bool.pick(
58 Glyph,
59 Char.is_eq(c, '"'),
60 DoubleQuote{},
61 Bool.pick(
62 Glyph,
63 Char.is_eq(c, '\''),
64 SingleQuote{},
65 Bool.pick(
66 Glyph,
67 Char.is_eq(c, '\\'),
68 Backslash{},
69 Bool.pick(
70 Glyph,
71 Char.is_eq(c, '\r'),
72 CarriageReturn{},
73 Bool.pick(Glyph, Char.is_eq(c, ' ') || Char.is_eq(c, '\t'), Blank{}, Other{})
74 )
75 )
76 )
77 )
78 )
79 )
80
81
82def Comment.none() -> Comment:
83 Comment{0, 0, 0, False{}, ""}
84
85
86def Comment.closed(open: Comment, at: U32) -> Comment:
87 match open:
88 case Comment{beg, end, line, trailing, body}:
89 Comment{beg, at, line, trailing, String.reverse(body)}
90
91def Comment.push(open: Comment, c: Char) -> Comment:
92 match open:
93 case Comment{beg, end, line, trailing, body}:
94 Comment{beg, end, line, trailing, SCon{c, body}}
95
96def Scan.in_code(
97 glyph: Glyph,
98 +at: U32,
99 +line: U32,
100 +code: Bool,
101 open: Comment,
102 comments: List<&2, Comment>,
103 lines: List<&2, Bool>
104) -> Scan:
105 match glyph:
106 case LineEnd{}:
107 Scan{InCode{}, (at + 1: U32), (line + 1: U32), False{}, open, comments, code <> lines}
108 case Hash{}:
109 Scan{InComment{}, (at + 1: U32), line, code, Comment{at, at, line, code, ""}, comments, lines}
110 case DoubleQuote{}:
111 Scan{InQuote{True{}, False{}}, (at + 1: U32), line, True{}, open, comments, lines}
112 case SingleQuote{}:
113 Scan{InQuote{False{}, False{}}, (at + 1: U32), line, True{}, open, comments, lines}
114 case Blank{}:
115 Scan{InCode{}, (at + 1: U32), line, code, open, comments, lines}
116 case CarriageReturn{}:
117 Scan{InCode{}, (at + 1: U32), line, code, open, comments, lines}
118 case _:
119 Scan{InCode{}, (at + 1: U32), line, True{}, open, comments, lines}
120
121def Scan.in_comment(
122 glyph: Glyph,
123 c: Char,
124 +at: U32,
125 line: U32,
126 code: Bool,
127 open: Comment,
128 comments: List<&2, Comment>,
129 lines: List<&2, Bool>
130) -> Scan:
131 match glyph:
132 case LineEnd{}:
133 Scan{
134 InCode{},
135 (at + 1: U32),
136 (line + 1: U32),
137 False{},
138 Comment.none(),
139 Comment.closed(open, at) <> comments,
140 code <> lines
141 }
142 case CarriageReturn{}:
143 Scan{
144 InCode{},
145 (at + 1: U32),
146 line,
147 code,
148 Comment.none(),
149 Comment.closed(open, at) <> comments,
150 lines
151 }
152 case _:
153 Scan{InComment{}, (at + 1: U32), line, code, Comment.push(open, c), comments, lines}
154
155
156def Scan.quote(string: Bool, double: Bool) -> Mode:
157 match string double:
158 case True{} True{}:
159 InCode{}
160 case False{} False{}:
161 InCode{}
162 case _ _:
163 InQuote{string, False{}}
164
165
166
167def Scan.in_quote(
168 string: Bool,
169 escaped: Bool,
170 glyph: Glyph,
171 at: U32,
172 line: U32,
173 code: Bool,
174 open: Comment,
175 comments: List<&2, Comment>,
176 lines: List<&2, Bool>
177) -> Scan:
178 match escaped glyph:
179 case _ LineEnd{}:
180 Scan{
181 InQuote{string, False{}},
182 (at + 1: U32),
183 (line + 1: U32),
184 True{},
185 open,
186 comments,
187 True{} <> lines
188 }
189 case True{} _:
190 Scan{InQuote{string, False{}}, (at + 1: U32), line, code, open, comments, lines}
191 case False{} Backslash{}:
192 Scan{InQuote{string, True{}}, (at + 1: U32), line, code, open, comments, lines}
193 case False{} DoubleQuote{}:
194 Scan{Scan.quote(string, True{}), (at + 1: U32), line, code, open, comments, lines}
195 case False{} SingleQuote{}:
196 Scan{Scan.quote(string, False{}), (at + 1: U32), line, code, open, comments, lines}
197 case False{} _:
198 Scan{InQuote{string, False{}}, (at + 1: U32), line, code, open, comments, lines}
199
200def Scan.step(glyph: Glyph, c: Char, s: Scan) -> Scan:
201 match s:
202 case Scan{InCode{}, at, line, code, open, comments, lines}:
203 Scan.in_code(glyph, at, line, code, open, comments, lines)
204 case Scan{InComment{}, at, line, code, open, comments, lines}:
205 Scan.in_comment(glyph, c, at, line, code, open, comments, lines)
206 case Scan{InQuote{string, escaped}, at, line, code, open, comments, lines}:
207 Scan.in_quote(string, escaped, glyph, at, line, code, open, comments, lines)
208
209def Scan.go(text: String, s: Scan) -> Scan:
210 match text:
211 case SNil{}:
212 s
213 case SCon{+c, t}:
214 Scan.go(t, Scan.step(Glyph.of(c), c, s))
215
216def Scan.done(s: Scan) -> Scanned:
217 match s:
218 case Scan{InComment{}, at, line, code, open, comments, lines}:
219 Scanned{
220 List.reverse(&2, Comment, Comment.closed(open, at) <> comments),
221 List.reverse(&2, Bool, code <> lines)
222 }
223 case Scan{mode, at, line, code, open, comments, lines}:
224 Scanned{List.reverse(&2, Comment, comments), List.reverse(&2, Bool, code <> lines)}
225
226def Scan.of(text: String) -> Scanned:
227 Scan.done(Scan.go(text, Scan{InCode{}, 0, 0, False{}, Comment.none(), [], []}))
228
229
230
231type Action is Data:
232 Disable{}
233 Expect{}
234
235type Scope is Data:
236 WholeFile{}
237 Begin{}
238 End{}
239 NextLine{}
240 SameLine{}
241
242type Keyword is Data:
243 Keyword{action: Action, scope: Scope}
244
245
246type Target is Data:
247 Target{rule: String, pinned: Maybe<&2, String>}
248
249
250type Draft is Data:
251 Draft{keyword: Keyword, target: Target, reasons: List<&2, String>}
252
253
254
255type Directive is Data:
256 Directive{
257 action: Action,
258 scope: Scope,
259 rule: String,
260 severity: Maybe<&2, Lint.Severity>,
261 comment: Comment,
262 index: U32
263 }
264
265
266type Said is Data:
267 Said{directive: Directive}
268 Wrong{comment: Comment, why: String}
269
270def Action.show(a: Action) -> String:
271 match a:
272 case Disable{}:
273 "disable"
274 case Expect{}:
275 "expect"
276
277def Scope.show(s: Scope) -> String:
278 match s:
279 case WholeFile{}:
280 "file"
281 case Begin{}:
282 "begin"
283 case End{}:
284 "end"
285 case NextLine{}:
286 "next"
287 case SameLine{}:
288 "line"
289
290def Action.names() -> List<&2, Names.Named<Action>>:
291 Names.table(~Action, ~Action.show, [Disable{}, Expect{}])
292
293def Scope.names() -> List<&2, Names.Named<Scope>>:
294 Names.table(~Scope, ~Scope.show, [WholeFile{}, Begin{}, End{}, NextLine{}, SameLine{}])
295
296def Severity.pinned(severity: Maybe<&2, Lint.Severity>) -> String:
297 Maybe.default(&2, String, Maybe.map(&2, Lint.Severity, String, Lint.Severity.show, severity), "")
298
299def Severity.is_eq(a: Maybe<&2, Lint.Severity>, b: Maybe<&2, Lint.Severity>) -> Bool:
300 String.eq(Severity.pinned(a), Severity.pinned(b))
301
302
303type Words is Data:
304 Words{word: String, out: List<&2, String>}
305
306def Words.step(blank: Bool, c: Char, w: Words) -> Words:
307 match blank w:
308 case True{} Words{SNil{}, out}:
309 Words{"", out}
310 case True{} Words{SCon{h, t}, out}:
311 Words{"", String.reverse(SCon{h, t}) <> out}
312 case False{} Words{word, out}:
313 Words{SCon{c, word}, out}
314
315def Words.go(s: String, w: Words) -> Words:
316 match s:
317 case SNil{}:
318 w
319 case SCon{+c, t}:
320 Words.go(t, Words.step(Char.is_eq(c, ' ') || Char.is_eq(c, '\t'), c, w))
321
322def Words.done(w: Words) -> List<&2, String>:
323 match w:
324 case Words{SNil{}, out}:
325 List.reverse(&2, String, out)
326 case Words{SCon{h, t}, out}:
327 List.reverse(&2, String, String.reverse(SCon{h, t}) <> out)
328
329def Words.of(s: String) -> List<&2, String>:
330 Words.done(Words.go(s, Words{"", []}))
331
332def Draft.kind(parts: List<&2, String>) -> Maybe<&2, Keyword>:
333 match parts:
334 case Con{a, Con{s, Nil{}}}:
335 do Maybe<&2, Keyword>:
336 action: Action <- Names.lookup(Action, a, Action.names())
337 scope: Scope <- Names.lookup(Scope, s, Scope.names())
338 return Keyword{action, scope}
339 case _:
340 None{}
341
342
343def Draft.is_rule_id(parts: List<&2, String>) -> Bool:
344 match parts:
345 case Con{a, rest}:
346 Bool.not(List.any(~&2, ~String, ~String.is_empty, Con{a, rest}))
347 case _:
348 False{}
349
350
351def Draft.target(parts: List<&2, String>) -> Maybe<&2, Target>:
352 match parts:
353 case Con{+rule, Nil{}}:
354 Lists.when(Target, Draft.is_rule_id(String.split(rule, '/')), Target{rule, None{}})
355 case Con{+rule, Con{p, rest}}:
356 Lists.when(
357 Target,
358 Draft.is_rule_id(String.split(rule, '/')),
359 Target{rule, Some{String.join(p <> rest, "@")}}
360 )
361 case Nil{}:
362 None{}
363
364
365def Draft.label() -> String:
366 "not-expected-because:"
367
368def Draft.part(part: List<&2, String>, out: List<&2, String>) -> List<&2, String>:
369 String.join(List.reverse(&2, String, part), " ") <> out
370
371
372def Draft.reasons.step(
373 dash: Bool,
374 word: String,
375 read: List<&2, String> & List<&2, String>
376) -> List<&2, String>
377 & List<&2, String>:
378 match dash read:
379 case True{} Tuple{part, out}:
380 ([], Draft.part(part, out))
381 case False{} Tuple{part, out}:
382 (word <> part, out)
383
384
385def Draft.reasons(
386 words: List<&2, String>,
387 read: List<&2, String> & List<&2, String>
388) -> List<&2, String>:
389 match words read:
390 case Nil{} Tuple{part, out}:
391 List.reverse(&2, String, Draft.part(part, out))
392 case Con{+w, rest} _:
393 Draft.reasons(rest, Draft.reasons.step(String.eq(w, Draft.label()), w, read))
394
395
396def Draft.reason(rest: List<&2, String>) -> Maybe<&2, List<&2, String>>:
397 match rest:
398 case Nil{}:
399 Some{[]}
400 case Con{dash, more}:
401 Lists.when(List<&2, String>, String.eq(dash, "--"), Draft.reasons(more, ([], [])))
402
403
404def Draft.of(words: List<&2, String>) -> Maybe<&2, Draft>:
405 match words:
406 case Con{kind, Con{named, rest}}:
407 do Maybe<&2, Draft>:
408 k: Keyword <- Draft.kind(String.split(kind, '-'))
409 t: Target <- Draft.target(String.split(named, '@'))
410 r: List<&2, String> <- Draft.reason(rest)
411 return Draft{k, t, r}
412 case _:
413 None{}
414
415def Problem.pinned(pinned: Maybe<&2, String>) -> Maybe<&2, String>:
416 match pinned:
417 case Some{+p}:
418 Lists.when(
419 String,
420 Maybe.is_none(&2, Lint.Severity, Names.lookup(Lint.Severity, p, Lint.Severity.names())),
421 p ++ " is not a severity."
422 )
423 case None{}:
424 None{}
425
426def Problem.severity(action: Action, pinned: Bool, rule: String) -> Maybe<&2, String>:
427 match action pinned:
428 case Disable{} True{}:
429 Some{"A disable takes no severity."}
430 case Expect{} False{}:
431 Some{"An expect pins a severity: " ++ rule ++ "@..."}
432 case _ _:
433 None{}
434
435def Problem.reason(scope: Scope, reasons: List<&2, String>) -> Maybe<&2, String>:
436 match scope reasons:
437 case End{} Nil{}:
438 None{}
439 case End{} _:
440 Some{"An end takes no reason; the begin has it."}
441 case _ Con{r, more}:
442 Lists.when(String, String.is_empty(r), "A reason is required, after `--`.")
443 case _ Nil{}:
444 Some{"A reason is required, after `--`."}
445
446def Problem.because(action: Action, scope: Scope, reasons: List<&2, String>) -> Maybe<&2, String>:
447 match action scope reasons:
448 case Disable{} End{} _:
449 None{}
450 case Disable{} _ Con{r, Con{b, more}}:
451 Lists.when(String, String.is_empty(b), "Say why it is not an expect, after " ++ Draft.label())
452 case Disable{} _ _:
453 Some{
454 "A disable says why it is not an expect, after its reason: " ++ Draft.label() ++ " <why>"
455 }
456 case _ _ _:
457 None{}
458
459def Problem.line(scope: Scope, trailing: Bool) -> Maybe<&2, String>:
460 match scope trailing:
461 case SameLine{} False{}:
462 Some{"A line directive must follow code on its line."}
463 case _ _:
464 None{}
465
466def Problem.first(problems: List<&2, Maybe<&2, String>>) -> Maybe<&2, String>:
467 match problems:
468 case Nil{}:
469 None{}
470 case Con{Some{why}, rest}:
471 Some{why}
472 case Con{None{}, rest}:
473 Problem.first(rest)
474
475def Rule.is_reserved(namespace: String) -> Bool:
476 String.eq(namespace, "bend-lint")
477
478def Said.checked(why: Maybe<&2, String>, draft: Draft, comment: Comment, index: U32) -> Said:
479 match why draft:
480 case Some{w} _:
481 Wrong{comment, w}
482 case None{} Draft{Keyword{action, scope}, Target{rule, pinned}, reasons}:
483 Said{
484 Directive{
485 action,
486 scope,
487 rule,
488 Maybe.bind(
489 &2,
490 String,
491 Lint.Severity,
492 pinned,
493 p => Names.lookup(Lint.Severity, p, Lint.Severity.names())
494 ),
495 comment,
496 index
497 }
498 }
499
500
501def Said.of(
502 draft: Maybe<&2, Draft>,
503 trailing: Bool,
504 comment: Comment,
505 known: List<&2, String>,
506 index: U32
507) -> Said:
508 match draft:
509 case None{}:
510 Wrong{
511 comment,
512 "Write `# bend-lint: <disable|expect>-<file|begin|end|next|line> <rule>[@severity] -- <reason>`, and, for a disable, "
513 ++ Draft.label()
514 ++ " <why> after it."
515 }
516 case Some{Draft{Keyword{+action, +scope}, Target{+rule, +pinned}, +reasons}}:
517 Said.checked(
518 Problem.first(
519 [
520 Problem.pinned(pinned),
521 Problem.severity(action, Maybe.is_some(&2, String, pinned), rule),
522 Problem.reason(scope, reasons),
523 Problem.because(action, scope, reasons),
524 Problem.line(scope, trailing),
525 Lists.when(
526 String,
527 Rule.is_reserved(Strings.before(rule, '/')),
528 rule ++ " findings cannot be suppressed."
529 ),
530 Lists.when(
531 String,
532 Bool.not(List.contains(~String, ~String.eq, known, rule)),
533 "There is no rule " ++ rule ++ "."
534 )
535 ]
536 ),
537 Draft{Keyword{action, scope}, Target{rule, pinned}, reasons},
538 comment,
539 index
540 )
541
542def Draft.read.if(marked: Bool, text: String) -> Maybe<&2, Maybe<&2, Draft>>:
543 match marked:
544 case True{}:
545 Some{Draft.of(Words.of(String.drop(text, 10n)))}
546 case False{}:
547 None{}
548
549
550def Draft.read(+body: String) -> Maybe<&2, Maybe<&2, Draft>>:
551 Draft.read.if(String.starts_with(String.trim_start(body), "bend-lint:"), String.trim_start(body))
552
553def Said.read.of(
554 draft: Maybe<&2, Maybe<&2, Draft>>,
555 +comment: Comment,
556 known: List<&2, String>,
557 index: U32
558) -> Maybe<&2, Said>:
559 match draft comment:
560 case Some{d} Comment{beg, end, line, +trailing, body}:
561 Some{Said.of(d, trailing, Comment{beg, end, line, trailing, body}, known, index)}
562 case None{} _:
563 None{}
564
565def Said.read(+comment: Comment, known: List<&2, String>, index: U32) -> Maybe<&2, Said>:
566 match comment:
567 case Comment{beg, end, line, trailing, +body}:
568 Said.read.of(Draft.read(body), Comment{beg, end, line, trailing, body}, known, index)
569
570
571def Said.all(
572 comments: List<&2, Comment>,
573 +known: List<&2, String>,
574 +index: U32,
575 out: List<&2, Said>
576) -> List<&2, Said>:
577 match comments:
578 case Nil{}:
579 List.reverse(&2, Said, out)
580 case Con{c, rest}:
581 Said.all(rest, known, (index + 1: U32), Lists.push(Said, Said.read(c, known, index), out))
582
583
584
585
586
587type Closed is Data:
588 Closed{index: U32, line: U32}
589
590
591type Pairing is Data:
592 Pairing{open: List<&2, Directive>, closed: List<&2, Closed>, wrong: List<&2, Said>}
593
594
595def Open.latest(
596 open: List<&2, Directive>,
597 found: Maybe<&2, Directive>,
598 +action: String,
599 +rule: String
600) -> Maybe<&2, Directive>:
601 match open found:
602 case _ Some{d}:
603 Some{d}
604 case Nil{} None{}:
605 None{}
606 case Con{Directive{+a, s, +r, sev, c, i}, rest} None{}:
607 Open.latest(
608 rest,
609 Lists.when(
610 Directive,
611 String.eq(Action.show(a), action) && String.eq(r, rule),
612 Directive{a, s, r, sev, c, i}
613 ),
614 action,
615 rule
616 )
617
618def Open.keep(drop: Bool, d: Directive, rest: List<&2, Directive>) -> List<&2, Directive>:
619 match drop:
620 case True{}:
621 rest
622 case False{}:
623 d <> rest
624
625def Open.without(open: List<&2, Directive>, +index: U32) -> List<&2, Directive>:
626 match open:
627 case Nil{}:
628 []
629 case Con{Directive{a, s, r, sev, c, +i}, rest}:
630 Open.keep(U32.is_eq(i, index), Directive{a, s, r, sev, c, i}, Open.without(rest, index))
631
632def Pairing.closing(
633 same: Bool,
634 +index: U32,
635 line: U32,
636 rule: String,
637 comment: Comment,
638 p: Pairing
639) -> Pairing:
640 match same p:
641 case True{} Pairing{open, closed, wrong}:
642 Pairing{Open.without(open, index), Closed{index, line} <> closed, wrong}
643 case False{} Pairing{open, closed, wrong}:
644 Pairing{open, closed, Wrong{comment, "This end closes no begin of " ++ rule ++ "."} <> wrong}
645
646def Pairing.close(begin: Maybe<&2, Directive>, end: Directive, p: Pairing) -> Pairing:
647 match begin end p:
648 case Some{
649 Directive{a, s, r, sev, c, i}
650 } Directive{ea, es, er, esev, Comment{b, e, +line, t, body}, ei} Pairing{open, closed, wrong}:
651 Pairing.closing(
652 Severity.is_eq(sev, esev),
653 i,
654 line,
655 er,
656 Comment{b, e, line, t, body},
657 Pairing{open, closed, wrong}
658 )
659 case None{} Directive{ea, es, er, esev, ec, ei} Pairing{open, closed, wrong}:
660 Pairing{open, closed, Wrong{ec, "This end closes no begin of " ++ er ++ "."} <> wrong}
661
662def Pairing.step(said: Said, p: Pairing) -> Pairing:
663 match said p:
664 case Said{Directive{a, Begin{}, r, sev, c, i}} Pairing{open, closed, wrong}:
665 Pairing{Directive{a, Begin{}, r, sev, c, i} <> open, closed, wrong}
666 case Said{Directive{+a, End{}, +r, sev, c, i}} Pairing{+open, closed, wrong}:
667 Pairing.close(
668 Open.latest(open, None{}, Action.show(a), r),
669 Directive{a, End{}, r, sev, c, i},
670 Pairing{open, closed, wrong}
671 )
672 case _ _:
673 p
674
675def Pairing.go(saids: List<&2, Said>, p: Pairing) -> Pairing:
676 match saids:
677 case Nil{}:
678 p
679 case Con{s, rest}:
680 Pairing.go(rest, Pairing.step(s, p))
681
682def Pairing.unclosed(open: List<&2, Directive>, wrong: List<&2, Said>) -> List<&2, Said>:
683 match open:
684 case Nil{}:
685 wrong
686 case Con{Directive{a, s, r, sev, c, i}, rest}:
687 Pairing.unclosed(rest, Wrong{c, "This begin of " ++ r ++ " is never closed."} <> wrong)
688
689
690
691
692
693type Covering is Data:
694 Covering{directive: Directive, from: U32, to: U32}
695
696
697def Lines.next_code(
698 lines: List<&2, Bool>,
699 found: Maybe<&2, U32>,
700 +i: U32,
701 +after: U32
702) -> Maybe<&2, U32>:
703 match lines found:
704 case _ Some{x}:
705 Some{x}
706 case Nil{} None{}:
707 None{}
708 case Con{code, rest} None{}:
709 Lines.next_code(rest, Lists.when(U32, code && U32.is_gt(i, after), i), (i + 1: U32), after)
710
711def Closed.find(closed: List<&2, Closed>, found: Maybe<&2, U32>, +index: U32) -> Maybe<&2, U32>:
712 match closed found:
713 case _ Some{line}:
714 Some{line}
715 case Nil{} None{}:
716 None{}
717 case Con{Closed{i, line}, rest} None{}:
718 Closed.find(rest, Lists.when(U32, U32.is_eq(i, index), line), index)
719
720def Covering.next(d: Directive, found: Maybe<&2, U32>) -> Covering:
721 match found:
722 case Some{+l}:
723 Covering{d, l, l}
724 case None{}:
725 Covering{d, 1, 0}
726
727
728def Covering.of(
729 said: Said,
730 lines: List<&2, Bool>,
731 closed: List<&2, Closed>,
732 last: U32
733) -> Maybe<&2, Covering>:
734 match said:
735 case Said{Directive{a, WholeFile{}, r, sev, c, i}}:
736 Some{Covering{Directive{a, WholeFile{}, r, sev, c, i}, 0, last}}
737 case Said{Directive{a, SameLine{}, r, sev, Comment{b, e, +l, t, body}, i}}:
738 Some{Covering{Directive{a, SameLine{}, r, sev, Comment{b, e, l, t, body}, i}, l, l}}
739 case Said{Directive{a, NextLine{}, r, sev, Comment{b, e, +l, t, body}, i}}:
740 Some{
741 Covering.next(
742 Directive{a, NextLine{}, r, sev, Comment{b, e, l, t, body}, i},
743 Lines.next_code(lines, None{}, 0, l)
744 )
745 }
746 case Said{Directive{a, Begin{}, r, sev, Comment{b, e, +l, t, body}, +i}}:
747 Some{
748 Covering{
749 Directive{a, Begin{}, r, sev, Comment{b, e, l, t, body}, i},
750 l,
751 Maybe.default(&2, U32, Closed.find(closed, None{}, i), last)
752 }
753 }
754 case _:
755 None{}
756
757def Covering.all(
758 saids: List<&2, Said>,
759 +lines: List<&2, Bool>,
760 +closed: List<&2, Closed>,
761 +last: U32,
762 out: List<&2, Covering>
763) -> List<&2, Covering>:
764 match saids:
765 case Nil{}:
766 List.reverse(&2, Covering, out)
767 case Con{s, rest}:
768 Covering.all(
769 rest,
770 lines,
771 closed,
772 last,
773 Lists.push(Covering, Covering.of(s, lines, closed, last), out)
774 )
775
776def Covering.pins(
777 action: Action,
778 pinned: Maybe<&2, Lint.Severity>,
779 severity: Lint.Severity
780) -> Bool:
781 match action:
782 case Disable{}:
783 True{}
784 case Expect{}:
785 Severity.is_eq(pinned, Some{severity})
786
787
788def Covering.covers(rule: String, severity: Lint.Severity, +row: U32, covering: Covering) -> Bool:
789 match covering:
790 case Covering{Directive{action, scope, r, sev, c, i}, from, to}:
791 String.eq(
792 r,
793 rule
794 )
795 && U32.is_le(from, row)
796 && U32.is_le(row, to)
797 && Covering.pins(action, sev, severity)
798
799def Covering.hit(hit: Bool, c: Covering, out: List<&2, U32>) -> List<&2, U32>:
800 match hit c:
801 case True{} Covering{Directive{a, s, r, sev, comment, i}, from, to}:
802 i <> out
803 case False{} _:
804 out
805
806def Covering.hits(
807 coverings: List<&2, Covering>,
808 +rule: String,
809 +severity: Lint.Severity,
810 +row: U32,
811 out: List<&2, U32>
812) -> List<&2, U32>:
813 match coverings:
814 case Nil{}:
815 out
816 case Con{+c, rest}:
817 Covering.hits(
818 rest,
819 rule,
820 severity,
821 row,
822 Covering.hit(Covering.covers(rule, severity, row, c), c, out)
823 )
824
825
826
827
828
829
830type Applied is Data:
831 Applied{kept: List<&2, Lint.Found>, suppressed: List<&2, Lint.Found>, used: List<&2, U32>}
832
833
834
835type Suppressed is Data:
836 Suppressed{kept: List<&2, Lint.Found>, suppressed: List<&2, Lint.Found>}
837
838def Applied.add(hits: List<&2, U32>, found: Lint.Found, a: Applied) -> Applied:
839 match hits a:
840 case Nil{} Applied{kept, suppressed, used}:
841 Applied{found <> kept, suppressed, used}
842 case Con{h, t} Applied{kept, suppressed, used}:
843 Applied{kept, found <> suppressed, Lists.onto(&2, U32, Con{h, t}, used)}
844
845
846def Applied.go(
847 founds: List<&2, Lint.Found>,
848 at: List<&2, Text.Position>,
849 +coverings: List<&2, Covering>,
850 a: Applied
851) -> Applied:
852 match founds at:
853 case Con{
854 Lint.Found{+rule, Lint.Finding{+severity, span, message, fixes}},
855 rest
856 } Con{Text.Position{line, column}, more}:
857 Applied.go(
858 rest,
859 more,
860 coverings,
861 Applied.add(
862 Covering.hits(coverings, rule, severity, (line - 1: U32), []),
863 Lint.Found{rule, Lint.Finding{severity, span, message, fixes}},
864 a
865 )
866 )
867 case _ _:
868 a
869
870def Unmet.add(
871 used: Bool,
872 action: Action,
873 rule: String,
874 pinned: Maybe<&2, Lint.Severity>,
875 span: Lint.Span,
876 out: List<&2, Lint.Found>
877) -> List<&2, Lint.Found>:
878 match used action:
879 case True{} _:
880 out
881 case False{} Expect{}:
882 Lint.Found{
883 "bend-lint/unmet-expectation",
884 Lint.Finding{
885 Lint.Error{},
886 span,
887 "Expected a "
888 ++ rule
889 ++ "@"
890 ++ Severity.pinned(pinned)
891 ++ " finding on the lines this covers; none.",
892 []
893 }
894 }
895 <> out
896 case False{} Disable{}:
897 Lint.Found{
898 "bend-lint/unused-disable",
899 Lint.Finding{
900 Lint.Warning{},
901 span,
902 "This disables " ++ rule ++ ", but nothing on the lines it covers needs it.",
903 []
904 }
905 }
906 <> out
907
908
909def Unmet.go(
910 coverings: List<&2, Covering>,
911 +used: List<&2, U32>,
912 +path: String,
913 out: List<&2, Lint.Found>
914) -> List<&2, Lint.Found>:
915 match coverings:
916 case Nil{}:
917 List.reverse(&2, Lint.Found, out)
918 case Con{Covering{Directive{a, s, r, sev, Comment{b, e, l, t, body}, i}, from, to}, rest}:
919 Unmet.go(
920 rest,
921 used,
922 path,
923 Unmet.add(List.contains(~U32, ~U32.is_eq, used, i), a, r, sev, Lint.Span{path, b, e}, out)
924 )
925
926def Wrong.found(
927 saids: List<&2, Said>,
928 +path: String,
929 out: List<&2, Lint.Found>
930) -> List<&2, Lint.Found>:
931 match saids:
932 case Nil{}:
933 List.reverse(&2, Lint.Found, out)
934 case Con{Wrong{Comment{b, e, l, t, body}, why}, rest}:
935 Wrong.found(
936 rest,
937 path,
938 Lint.Found{
939 "bend-lint/directive",
940 Lint.Finding{Lint.Error{}, Lint.Span{path, b, e}, why, []}
941 }
942 <> out
943 )
944 case Con{s, rest}:
945 Wrong.found(rest, path, out)
946
947
948
949
950
951def Found.offsets(founds: List<&2, Lint.Found>, out: List<&2, U32>) -> List<&2, U32>:
952 match founds:
953 case Nil{}:
954 List.reverse(&2, U32, out)
955 case Con{
956 Lint.Found{rule, Lint.Finding{severity, Lint.Span{path, beg, end}, message, fixes}},
957 rest
958 }:
959 Found.offsets(rest, beg <> out)
960
961
962
963type Linted is Data:
964 Linted{path: String, text: String, known: List<&2, String>, ran: List<&2, String>}
965
966def Linted.path(linted: Linted) -> String:
967 match linted:
968 case Linted{path, text, known, ran}:
969 path
970
971def Linted.text(linted: Linted) -> String:
972 match linted:
973 case Linted{path, text, known, ran}:
974 text
975
976def Linted.known(linted: Linted) -> List<&2, String>:
977 match linted:
978 case Linted{path, text, known, ran}:
979 known
980
981def Linted.ran(linted: Linted) -> List<&2, String>:
982 match linted:
983 case Linted{path, text, known, ran}:
984 ran
985
986def Covering.keep(ran: Bool, c: Covering, out: List<&2, Covering>) -> List<&2, Covering>:
987 match ran:
988 case True{}:
989 c <> out
990 case False{}:
991 out
992
993def Covering.ran(
994 coverings: List<&2, Covering>,
995 +ran: List<&2, String>,
996 out: List<&2, Covering>
997) -> List<&2, Covering>:
998 match coverings:
999 case Nil{}:
1000 List.reverse(&2, Covering, out)
1001 case Con{Covering{Directive{a, s, +r, sev, c, i}, from, to}, rest}:
1002 Covering.ran(
1003 rest,
1004 ran,
1005 Covering.keep(
1006 List.contains(~String, ~String.eq, ran, r),
1007 Covering{Directive{a, s, r, sev, c, i}, from, to},
1008 out
1009 )
1010 )
1011
1012def done(
1013 a: Applied,
1014 coverings: List<&2, Covering>,
1015 unpaired: List<&2, Said>,
1016 saids: List<&2, Said>,
1017 +linted: Linted
1018) -> Suppressed:
1019 match a:
1020 case Applied{kept, suppressed, used}:
1021 Suppressed{
1022 Lists.concat(
1023 &2,
1024 Lint.Found,
1025 [
1026 List.reverse(&2, Lint.Found, kept),
1027 Wrong.found(saids, Linted.path(linted), []),
1028 Wrong.found(List.reverse(&2, Said, unpaired), Linted.path(linted), []),
1029 Unmet.go(Covering.ran(coverings, Linted.ran(linted), []), used, Linted.path(linted), [])
1030 ]
1031 ),
1032 List.reverse(&2, Lint.Found, suppressed)
1033 }
1034
1035def covered(
1036 +coverings: List<&2, Covering>,
1037 unpaired: List<&2, Said>,
1038 saids: List<&2, Said>,
1039 +linted: Linted,
1040 +founds: List<&2, Lint.Found>
1041) -> Suppressed:
1042 done(
1043 Applied.go(
1044 founds,
1045 Text.positions(Linted.text(linted), Found.offsets(founds, [])),
1046 coverings,
1047 Applied{[], [], []}
1048 ),
1049 coverings,
1050 unpaired,
1051 saids,
1052 linted
1053 )
1054
1055def paired(
1056 p: Pairing,
1057 +saids: List<&2, Said>,
1058 +lines: List<&2, Bool>,
1059 linted: Linted,
1060 founds: List<&2, Lint.Found>
1061) -> Suppressed:
1062 match p:
1063 case Pairing{open, closed, wrong}:
1064 covered(
1065 Covering.all(
1066 saids,
1067 lines,
1068 closed,
1069 (U32.from_nat(Lists.length(&2, Bool, lines)) - 1: U32),
1070 []
1071 ),
1072 Pairing.unclosed(open, wrong),
1073 saids,
1074 linted,
1075 founds
1076 )
1077
1078def said(
1079 +saids: List<&2, Said>,
1080 lines: List<&2, Bool>,
1081 linted: Linted,
1082 founds: List<&2, Lint.Found>
1083) -> Suppressed:
1084 paired(Pairing.go(saids, Pairing{[], [], []}), saids, lines, linted, founds)
1085
1086def scanned(scanned: Scanned, +linted: Linted, founds: List<&2, Lint.Found>) -> Suppressed:
1087 match scanned:
1088 case Scanned{comments, lines}:
1089 said(Said.all(comments, Linted.known(linted), 0, []), lines, linted, founds)
1090
1091def when(has: Bool, +linted: Linted, founds: List<&2, Lint.Found>) -> Suppressed:
1092 match has:
1093 case True{}:
1094 scanned(Scan.of(Linted.text(linted)), linted, founds)
1095 case False{}:
1096 Suppressed{founds, []}
1097
1098
1099def has_mark(text: String, found: Bool) -> Bool:
1100 match text found:
1101 case _ True{}:
1102 True{}
1103 case SNil{} False{}:
1104 False{}
1105 case SCon{'b', +rest} False{}:
1106 has_mark(rest, String.starts_with(rest, "end-lint:"))
1107 case SCon{c, rest} False{}:
1108 has_mark(rest, False{})
1109
1110def apply(+linted: Linted, founds: List<&2, Lint.Found>) -> Suppressed:
1111 when(has_mark(Linted.text(linted), False{}), linted, founds)