src/noqa.bend

bolt@1.26.0.0 · 252 lines · 0 definitions

1# src/noqa: `# noqa: CODE` comments, and the filter they make, after ruff.
2# A comment token (the lexer's, so never a string's contents) whose text is
3# `#`, any spaces, then `noqa` is a mark. A `:` right after `noqa` opens
4# its codes: runs of ASCII letters and digits, split by commas with spaces
5# around them allowed, the list ending at anything else, so free text may
6# follow (`# noqa: L001 IO entry point`). `noqa` at the comment's end, or
7# followed by a space, names no code: a bare mark. Codes are case-sensitive
8# and are src/codes.bend's (`L001`, `U002`). A finding is silenced when a
9# mark of its own file, on its own line, names its code.
10#
11# No rule reads this: the planner (lint/plan.bend) and the language server
12# run `keep` over the graded findings once every rule has run, the project
13# rules included, and then `noqa` (rules/style/noqa.bend) over what the
14# marks silenced. Pure (GPU-safe).
15import Base
16import bolt@1.26.0.0/src/lazy/lazy.bend as Lazy
17import bolt@1.26.0.0/src/syntax/lex.bend as Lex
18import bolt@1.26.0.0/src/rchars.bend as Rchars
19import bolt@1.26.0.0/src/finding.bend as F
20import bolt@1.26.0.0/src/codes.bend as Codes
21import bolt@1.26.0.0/src/src.bend as Src
22
23# one noqa comment: its line and column (0-based) and width, and the codes
24# it names, none for a bare one
25type Mark is Data:
26  Mark{line: U32, col: U32, len: U32, codes: List<&2, String>}
27
28# a file's noqa comments
29type Marks is Data:
30  Marks{path: String, marks: List<&2, Mark>}
31
32# ---------------------------------------------------------------------------
33# reading a comment
34
35# what a char is to the code list: a space, a comma, a letter or digit, or
36# anything else
37type Cls is Data:
38  CSpace{}
39  CComma{}
40  CCode{}
41  COther{}
42
43# a char's class
44def cls(+cc: Char) -> Cls:
45  +code = Bool.or(Char.is_alpha(cc), Char.is_digit(cc))
46  +tail = Bool.pick(Cls, code, CCode{}, COther{})
47  Bool.pick(Cls, Char.is_eq(cc, ' '), CSpace{}, Bool.pick(Cls, Char.is_eq(cc, ','), CComma{}, tail))
48
49# where the code list reader stands: before a code (spaces allowed), inside
50# one, after one (spaces, then a comma or the list's end), or past the list
51type Phase is Data:
52  Before{}
53  Inside{}
54  After{}
55  Past{}
56
57# the reader: where it stands, the code it is reading (its chars, the last
58# first), and the codes read (the last first)
59type Rd is Data:
60  Rd{phase: Phase, buf: List<&2, Char>, acc: List<&2, String>}
61
62# the chars read, the last first, as a code
63def word(buf: List<&2, Char>) -> String:
64  Rchars.text(buf)
65
66# before a code: a space waits, a letter or digit starts one, anything else
67# ends the list
68def step.before(kk: Cls, +cc: Char, +acc: List<&2, String>) -> Rd:
69  match kk:
70    case CSpace{}:
71      Rd{Before{}, [], acc}
72    case CComma{}:
73      Rd{Past{}, [], acc}
74    case CCode{}:
75      Rd{Inside{}, [cc], acc}
76    case COther{}:
77      Rd{Past{}, [], acc}
78
79# inside a code: a letter or digit extends it; a space, a comma or anything
80# else ends it, and a comma expects the next
81def step.inside(kk: Cls, +cc: Char, +buf: List<&2, Char>, +acc: List<&2, String>) -> Rd:
82  match kk:
83    case CSpace{}:
84      Rd{After{}, [], word(buf) <> acc}
85    case CComma{}:
86      Rd{Before{}, [], word(buf) <> acc}
87    case CCode{}:
88      Rd{Inside{}, cc <> buf, acc}
89    case COther{}:
90      Rd{Past{}, [], word(buf) <> acc}
91
92# after a code: a space waits, a comma expects the next, anything else ends
93# the list
94def step.after(kk: Cls, +acc: List<&2, String>) -> Rd:
95  match kk:
96    case CSpace{}:
97      Rd{After{}, [], acc}
98    case CComma{}:
99      Rd{Before{}, [], acc}
100    case CCode{}:
101      Rd{Past{}, [], acc}
102    case COther{}:
103      Rd{Past{}, [], acc}
104
105# the reader after one more char
106def step.at(ph: Phase, +cc: Char, +buf: List<&2, Char>, +acc: List<&2, String>) -> Rd:
107  match ph:
108    case Before{}:
109      step.before(cls(cc), cc, acc)
110    case Inside{}:
111      step.inside(cls(cc), cc, buf, acc)
112    case After{}:
113      step.after(cls(cc), acc)
114    case Past{}:
115      Rd{Past{}, buf, acc}
116
117# the reader after one more char
118def step(+cc: Char, rd: Rd) -> Rd:
119  Rd{ph, buf, acc} = rd
120  step.at(ph, cc, buf, acc)
121
122# the reader after the text
123def run(tt: String, rd: Rd) -> Rd:
124  match tt:
125    case SNil{}:
126      rd
127    case SCon{cc, rest}:
128      run(rest, step(cc, rd))
129
130# the codes read, in order, the one still being read included
131def finish.at(ph: Phase, +buf: List<&2, Char>, +acc: List<&2, String>) -> List<&2, String>:
132  match ph:
133    case Before{}:
134      List.reverse(&2, String, acc)
135    case Inside{}:
136      List.reverse(&2, String, word(buf) <> acc)
137    case After{}:
138      List.reverse(&2, String, acc)
139    case Past{}:
140      List.reverse(&2, String, acc)
141
142# the codes read, in order
143def finish(rd: Rd) -> List<&2, String>:
144  Rd{ph, buf, acc} = rd
145  finish.at(ph, buf, acc)
146
147# the codes a text after `noqa:` names
148def codes(tt: String) -> List<&2, String>:
149  finish(run(tt, Rd{Before{}, [], []}))
150
151# the text after its leading spaces
152def lead(tt: String) -> String:
153  match tt:
154    case SCon{' ', rest}:
155      lead(rest)
156    case other:
157      other
158
159# a comment's text past `noqa`: a mark naming the codes after a `:`, a bare
160# one at the end or before a space, and none for anything else (`noqas`)
161def mark.rest(+line: U32, +col: U32, +len: U32, tt: String) -> List<&2, Mark>:
162  match tt:
163    case SNil{}:
164      [Mark{line, col, len, []}]
165    case SCon{':', rest}:
166      [Mark{line, col, len, codes(rest)}]
167    case SCon{' ', _rest}:
168      [Mark{line, col, len, []}]
169    case other:
170      []
171
172# a comment's text past `#` and its spaces: a mark when it opens with `noqa`
173def mark.word(+line: U32, +col: U32, +len: U32, tt: String) -> List<&2, Mark>:
174  match tt:
175    case SCon{'n', SCon{'o', SCon{'q', SCon{'a', rest}}}}:
176      mark.rest(line, col, len, rest)
177    case other:
178      []
179
180# a comment's text: a mark when it is `#`, any spaces, then `noqa`
181def mark.text(+line: U32, +col: U32, +len: U32, tt: String) -> List<&2, Mark>:
182  match tt:
183    case SCon{'#', rest}:
184      mark.word(line, col, len, lead(rest))
185    case other:
186      []
187
188# a comment token's mark, when it is one; any other token has none
189def mark.of(tk: Lex.Tok) -> List<&2, Mark>:
190  match tk:
191    case Lex.Tok{Lex.TComment{}, +t, l, c}:
192      mark.text(l, c, U32.from_nat(String.length(t)), t)
193    case other:
194      []
195
196# the marks among the tokens, in order
197def marks(toks: List<&2, Lex.Tok>) -> List<&2, Mark>:
198  match toks:
199    case Nil{}:
200      Nil{}
201    case Con{t, rest}:
202      List.append(&2, Mark, mark.of(t), marks(rest))
203
204# a file's marks, from its tokens
205def file(path: String, toks: List<&2, Lex.Tok>) -> Marks:
206  Marks{path, marks(toks)}
207
208# each source's marks, in order
209def of_srcs(ss: List<&2, Src.Src>) -> List<&2, Marks>:
210  match ss:
211    case Nil{}:
212      Nil{}
213    case Con{Src.Src{path, _text, toks, _tree, _bound, _items}, rest}:
214      file(path, toks) <> of_srcs(rest)
215
216# ---------------------------------------------------------------------------
217# the filter
218
219# the codes include this one
220def named(cs: List<&2, String>, +code: String) -> Bool:
221  match cs:
222    case Nil{}:
223      False{}
224    case Con{c, rest}:
225      Lazy.or_else(String.eq(c, code), _u => named(rest, code))
226
227# a mark on the line names the code
228def on_line(mks: List<&2, Mark>, +line: U32, +code: String) -> Bool:
229  match mks:
230    case Nil{}:
231      False{}
232    case Con{Mark{l, _col, _len, cs}, rest}:
233      Lazy.or_else(Lazy.and_then(U32.is_eq(l, line), _u => named(cs, code)), _v => on_line(rest, line, code))
234
235# a mark of the file at the path, on the line, names the code
236def silenced(ms: List<&2, Marks>, +path: String, +line: U32, +code: String) -> Bool:
237  match ms:
238    case Nil{}:
239      False{}
240    case Con{Marks{p, mks}, rest}:
241      Lazy.or_else(Lazy.and_then(String.eq(p, path), _u => on_line(mks, line, code)),
242        _v => silenced(rest, path, line, code))
243
244# the graded findings no mark silences, in order
245def keep(+ms: List<&2, Marks>, gs: List<&2, F.Graded>) -> List<&2, F.Graded>:
246  match gs:
247    case Nil{}:
248      Nil{}
249    case Con{F.Graded{ll, F.Finding{+path, +line, col, len, +rule, msg}}, rest}:
250      +more = keep(ms, rest)
251      Bool.pick(List<&2, F.Graded>, silenced(ms, path, line, Codes.code(rule)), more,
252        F.Graded{ll, F.Finding{path, line, col, len, rule, msg}} <> more)