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). 15 import Base 16 import ./lazy/lazy.bend as Lazy 17 import ./syntax/lex.bend as Lex 18 import ./rchars.bend as Rchars 19 import ./finding.bend as F 20 import ./codes.bend as Codes 21 import ./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 25 type Mark is Data: 26 Mark{line: U32, col: U32, len: U32, codes: List<&2, String>} 27 28 # a file's noqa comments 29 type 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 37 type Cls is Data: 38 CSpace{} 39 CComma{} 40 CCode{} 41 COther{} 42 43 # a char's class 44 def 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 51 type 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) 59 type 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 63 def 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 68 def 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 81 def 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 94 def 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 106 def 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 118 def 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 123 def 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 131 def 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 143 def 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 148 def codes(tt: String) -> List<&2, String>: 149 finish(run(tt, Rd{Before{}, [], []})) 150 151 # the text after its leading spaces 152 def 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`) 161 def 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` 173 def 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` 181 def 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 189 def 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 197 def 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 205 def file(path: String, toks: List<&2, Lex.Tok>) -> Marks: 206 Marks{path, marks(toks)} 207 208 # each source's marks, in order 209 def 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 220 def 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 228 def 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 236 def 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 245 def 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)