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)