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