src/rules/style/space.bend
bolt@1.25.0.0 · 94 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. 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.
10import Base
11import bolt@1.25.0.0/src/src.bend as Src
12import bolt@1.25.0.0/src/finding.bend as F
13import bolt@1.25.0.0/src/syntax/lex.bend as Lex
14import bolt@1.25.0.0/src/lazy/lazy.bend as Lazy
15
16# is there a tab among the chars?
17def 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?
25def 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
35def 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
47def 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
52def 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
65def 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
76def 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
83def 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
91def 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))