src/rules/style/space.bend

bolt@1.17.0.0 · 78 lines · 0 definitions

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.
9import Base
10import bolt@1.17.0.0/src/src.bend as Src
11import bolt@1.17.0.0/src/finding.bend as F
12import bolt@1.17.0.0/src/syntax/lex.bend as Lex
13import bolt@1.17.0.0/src/lazy/lazy.bend as Lazy
14
15# is there a tab among the chars?
16def 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?
24def 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
34def 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
46def 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
51def 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
61def 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
68def 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
76def 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))