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