1 import Base 2 import bend-lint-api@0.2.1.0/shared/lists.bend as Lists 3 import bend-lint-api@0.2.1.0/shared/names.bend as Names 4 import bend-lint-api@0.2.1.0/shared/strings.bend as Strings 5 import ../text/main.bend as Text 6 import bend-lint-api@0.2.1.0/linter/lint.bend as Lint 7 8 # Scanning 9 # ======== 10 11 # A comment: where it is (code points), its line (from 0), whether code comes before it on its line, and its text 12 # after the #. 13 type Comment is Data: 14 Comment{beg: U32, end: U32, line: U32, trailing: Bool, body: String} 15 16 type Glyph is Data: 17 LineEnd{} 18 CarriageReturn{} 19 Hash{} 20 DoubleQuote{} 21 SingleQuote{} 22 Backslash{} 23 Blank{} 24 Other{} 25 26 # Where the scan is: in code, a comment, or a string or char literal (`string`), maybe just after a \ in it. 27 type Mode is Data: 28 InCode{} 29 InComment{} 30 InQuote{string: Bool, escaped: Bool} 31 32 # The scan so far: where it is, its offset and line, whether its line holds code, the comment it is in (its body last 33 # first), the comments before (last first), and whether each line before holds code (last first). 34 type 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 45 type Scanned is Data: 46 Scanned{comments: List<&2, Comment>, lines: List<&2, Bool>} 47 48 def 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 # No comment: what the scan holds as its open comment outside one. 82 def Comment.none() -> Comment: 83 Comment{0, 0, 0, False{}, ""} 84 85 # The comment `open`, ended at `at`, its body in order. 86 def 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 91 def 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 96 def 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 121 def 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 # Where a quote leaves the scan: a string's " and a char's ' close it; the other quote does not. 156 def 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 # A step in a string or a char literal (`string`): a \ escapes the next character, its closing quote ends it, and a 166 # line it spans holds code. 167 def 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 200 def 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 209 def 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 216 def 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 226 def Scan.of(text: String) -> Scanned: 227 Scan.done(Scan.go(text, Scan{InCode{}, 0, 0, False{}, Comment.none(), [], []})) 228 229 # Parsing 230 # ======= 231 type Action is Data: 232 Disable{} 233 Expect{} 234 235 type Scope is Data: 236 WholeFile{} 237 Begin{} 238 End{} 239 NextLine{} 240 SameLine{} 241 242 type Keyword is Data: 243 Keyword{action: Action, scope: Scope} 244 245 # What a directive names: a rule, and the severity it pins, as written. 246 type Target is Data: 247 Target{rule: String, pinned: Maybe<&2, String>} 248 249 # A directive as written, before its severity and rule are checked: its reasons are the texts after each `--`. 250 type Draft is Data: 251 Draft{keyword: Keyword, target: Target, reasons: List<&2, String>} 252 253 # A directive: what it does, how far, the rule it names, the severity it pins, its comment, and its place among the 254 # file's comments. 255 type 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 # A comment that says bend-lint: a directive, or what is wrong with it. 266 type Said is Data: 267 Said{directive: Directive} 268 Wrong{comment: Comment, why: String} 269 270 def Action.show(a: Action) -> String: 271 match a: 272 case Disable{}: 273 "disable" 274 case Expect{}: 275 "expect" 276 277 def 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 290 def Action.names() -> List<&2, Names.Named>: 291 Names.table(~Action, ~Action.show, [Disable{}, Expect{}]) 292 293 def Scope.names() -> List<&2, Names.Named>: 294 Names.table(~Scope, ~Scope.show, [WholeFile{}, Begin{}, End{}, NextLine{}, SameLine{}]) 295 296 def Severity.pinned(severity: Maybe<&2, Lint.Severity>) -> String: 297 Maybe.default(&2, String, Maybe.map(&2, Lint.Severity, String, Lint.Severity.show, severity), "") 298 299 def 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 # The words of a text, split at blanks: the word so far, last first, and the words before it, last first. 303 type Words is Data: 304 Words{word: String, out: List<&2, String>} 305 306 def 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 315 def 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 322 def 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 329 def Words.of(s: String) -> List<&2, String>: 330 Words.done(Words.go(s, Words{"", []})) 331 332 def 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 # Whether a rule id is one or more non-empty parts, split by /: its path in bend-lint-rules/. 343 def 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 # `rule@severity`, split at its @s. 351 def 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 # The word that starts a disable's second reason: why it is not an expect. 365 def Draft.label() -> String: 366 "not-expected-because:" 367 368 def Draft.part(part: List<&2, String>, out: List<&2, String>) -> List<&2, String>: 369 String.join(List.reverse(&2, String, part), " ") <> out 370 371 # The reason so far (last first) and those before it (last first), with `word` read: the label starts another. 372 def 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 # The reasons in `words`, split at each label, in order, after what is `read`. 385 def 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 # What comes after the rule: nothing, or `--` and the reasons. 396 def 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 # The draft of a directive's words, after `bend-lint:`. 404 def 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 415 def 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 426 def 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 435 def 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 446 def 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() ++ " " 455 } 456 case _ _ _: 457 None{} 458 459 def 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 466 def 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 475 def Rule.is_reserved(namespace: String) -> Bool: 476 String.eq(namespace, "bend-lint") 477 478 def 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 # What a comment that holds `draft` (or not) says, checked against the rules there are. 501 def 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: - [@severity] -- `, and, for a disable, " 513 ++ Draft.label() 514 ++ " 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 542 def 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 # What a comment's text says, if it says bend-lint:: a directive's draft, if it is one. 550 def 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 553 def 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 565 def 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 # What the comments say, in order, each numbered; `out` holds those so far, last first. 571 def 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 # Pairing 584 # ======= 585 586 # Where a begin was closed: its index, and the line of its end. 587 type Closed is Data: 588 Closed{index: U32, line: U32} 589 590 # The begins open so far (latest first), the begins closed, and what is wrong (last first). 591 type Pairing is Data: 592 Pairing{open: List<&2, Directive>, closed: List<&2, Closed>, wrong: List<&2, Said>} 593 594 # The latest open begin of an action and a rule. 595 def 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 618 def 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 625 def 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 632 def 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 646 def 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 662 def 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 675 def 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 682 def 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 # Covering 690 # ======== 691 692 # A directive and the lines it covers, both ends included (from 1 to 0 covers none). 693 type Covering is Data: 694 Covering{directive: Directive, from: U32, to: U32} 695 696 # The first line after `after` that holds code; `i` is the line of the head of `lines`. 697 def 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 711 def 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 720 def 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 # The lines a directive covers; `last` is the file's last line. 728 def 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 757 def 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 776 def 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 # Whether a covering covers a finding of `rule` and `severity` on line `row`. 788 def 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 799 def 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 806 def 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 # Applying 826 # ======== 827 828 # The findings so far: those that stay, those a directive covers (both last first), and the directives that covered 829 # one. 830 type Applied is Data: 831 Applied{kept: List<&2, Lint.Found>, suppressed: List<&2, Lint.Found>, used: List<&2, U32>} 832 833 # A file's findings after its directives: those that stay, then the findings about its directives, and those a 834 # directive covers. 835 type Suppressed is Data: 836 Suppressed{kept: List<&2, Lint.Found>, suppressed: List<&2, Lint.Found>} 837 838 def 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 # The findings, each with where it starts (`at`, in order), sorted by the directives that cover their lines. 846 def 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 870 def 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 # The findings about the directives that nothing used: an expect not met, a disable that covered nothing. 909 def 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 926 def 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 # Directives 948 # ========== 949 950 # Where each of `founds` starts, in order, onto `out` (last first). 951 def 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 # A linted file: its path and text, the ids of the rules there are, and those of the rules that ran on it. A rule 962 # turned off did not run, so its directives are never unused. 963 type Linted is Data: 964 Linted{path: String, text: String, known: List<&2, String>, ran: List<&2, String>} 965 966 def Linted.path(linted: Linted) -> String: 967 match linted: 968 case Linted{path, text, known, ran}: 969 path 970 971 def Linted.text(linted: Linted) -> String: 972 match linted: 973 case Linted{path, text, known, ran}: 974 text 975 976 def Linted.known(linted: Linted) -> List<&2, String>: 977 match linted: 978 case Linted{path, text, known, ran}: 979 known 980 981 def Linted.ran(linted: Linted) -> List<&2, String>: 982 match linted: 983 case Linted{path, text, known, ran}: 984 ran 985 986 def 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 993 def 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 1012 def 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 1035 def 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 1055 def 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 1078 def 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 1086 def 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 1091 def 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 # Whether `text` has a directive's mark, "bend-lint:". Only a 'b' starts a comparison, so it is one pass. 1099 def 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 1110 def apply(+linted: Linted, founds: List<&2, Lint.Found>) -> Suppressed: 1111 when(has_mark(Linted.text(linted), False{}), linted, founds)