1 # rule space: trailing whitespace, a tab, or a line over 120 characters. The 2 # width counts a string literal as two characters: a long message or a test 3 # fixture does not make a line hard to read, code does. A comment counts at 4 # its full width. Expectation trailers (`#|` lines) are data and exempt from 5 # the width only: trailing whitespace on a `#|` line is still reported. A tab 6 # is reported wherever it sits, inside a string literal included. A line 7 # with both trailing whitespace and a tab is reported once, for the 8 # whitespace. 9 import Base 10 import ../../src.bend as Src 11 import ../../finding.bend as F 12 import ../../syntax/lex.bend as Lex 13 import ../../lazy/lazy.bend as Lazy 14 15 # is there a tab among the chars? 16 def has_tab(cs: List<&2, Char>) -> Bool: 17 match cs: 18 case Nil{}: 19 False{} 20 case Con{c, t}: 21 Lazy.or_else(Char.is_eq(c, '\t'), _u => has_tab(t)) 22 23 # does the line end in whitespace? 24 def trailing(cs: List<&2, Char>) -> Bool: 25 match cs: 26 case Nil{}: 27 False{} 28 case Con{c, Nil{}}: 29 Char.is_space(c) 30 case Con{c, t}: 31 trailing(t) 32 33 # a token's share of its line's width; a `#|` trailer line counts nothing 34 def width_of(kk: Lex.TokKind, +tt: String) -> U32: 35 match kk: 36 case Lex.TStr{}: 37 2 38 case Lex.TNewline{}: 39 0 40 case Lex.TComment{}: 41 Bool.pick(U32, String.starts_with(tt, "#|"), 0, U32.from_nat(String.length(tt))) 42 case other: 43 U32.from_nat(String.length(tt)) 44 45 # a finding when the width is over 120 46 def too_wide(+width: U32, +path: String, +nn: U32, +more: List<&2, F.Finding>) -> List<&2, F.Finding>: 47 Bool.pick(List<&2, F.Finding>, U32.is_gt(width, 120), 48 F.Finding{path, nn, 120, 0, "space", "Line is longer than 120 characters."} <> more, more) 49 50 # one pass over the tokens: a newline closes a line's width 51 def widths(toks: List<&2, Lex.Tok>, +path: String, +nn: U32, +acc: U32) -> List<&2, F.Finding>: 52 match toks: 53 case Nil{}: 54 too_wide(acc, path, nn, []) 55 case Con{Lex.Tok{Lex.TNewline{}, t, l, c}, rest}: 56 too_wide(acc, path, nn, widths(rest, path, (nn + 1 : U32), 0)) 57 case Con{Lex.Tok{k, t, l, c}, rest}: 58 +w = width_of(k, t) 59 widths(rest, path, nn, (acc + w : U32)) 60 61 def check.line(+path: String, +nn: U32, +cs: List<&2, Char>, +more: List<&2, F.Finding>) -> List<&2, F.Finding>: 62 +len = U32.from_nat(List.length(&2, Char, cs)) 63 Bool.pick(List<&2, F.Finding>, trailing(cs), 64 F.Finding{path, nn, (len - 1 : U32), 1, "space", "Line has trailing whitespace."} <> more, 65 Bool.pick(List<&2, F.Finding>, has_tab(cs), 66 F.Finding{path, nn, 0, 1, "space", "Line contains a tab; indent with spaces."} <> more, more)) 67 68 def check.go(lines: List<&2, String>, +path: String, +nn: U32) -> List<&2, F.Finding>: 69 match lines: 70 case Nil{}: 71 Nil{} 72 case Con{line, rest}: 73 check.line(path, nn, String.to_list(line), check.go(rest, path, (nn + 1 : U32))) 74 75 # the rule 76 def check(ss: Src.Src) -> List<&2, F.Finding>: 77 Src.Src{+path, text, toks, tree, bound, items} = ss 78 List.append(&2, F.Finding, check.go(String.lines(text), path, 0), widths(toks, path, 0, 0))