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)