3
3
from __future__ import annotations
4
4
5
5
import ast
6
+ import functools
7
+ import re
6
8
from abc import ABC
7
9
from typing import TYPE_CHECKING , Any , Union
8
10
13
15
14
16
if TYPE_CHECKING :
15
17
from collections .abc import Iterable
18
+ from re import Match
16
19
17
20
from ..runner import SharedState
18
21
@@ -167,7 +170,7 @@ def __init__(self, shared_state: SharedState):
167
170
self .__state = shared_state
168
171
169
172
self .options = self .__state .options
170
- self .noqas = self . __state . noqas
173
+ self .noqas : dict [ int , set [ str ]] = {}
171
174
172
175
def get_state (self , * attrs : str , copy : bool = False ) -> dict [str , Any ]:
173
176
# require attrs, since we inherit a *ton* of stuff which we don't want to copy
@@ -197,12 +200,19 @@ def save_state(self, node: cst.CSTNode, *attrs: str, copy: bool = False):
197
200
def restore_state (self , node : cst .CSTNode ):
198
201
self .set_state (self .outer .pop (node , {}))
199
202
203
+ def is_noqa (self , node : cst .CSTNode , code : str ):
204
+ if self .options .disable_noqa :
205
+ return False
206
+ pos = self .get_metadata (PositionProvider , node ).start
207
+ noqas = self .noqas .get (pos .line )
208
+ return noqas is not None and (noqas == set () or code in noqas )
209
+
200
210
def error (
201
211
self ,
202
212
node : cst .CSTNode ,
203
213
* args : str | Statement | int ,
204
214
error_code : str | None = None ,
205
- ):
215
+ ) -> bool :
206
216
if error_code is None :
207
217
assert (
208
218
len (self .error_codes ) == 1
@@ -211,9 +221,12 @@ def error(
211
221
# don't emit an error if this code is disabled in a multi-code visitor
212
222
# TODO: write test for only one of 910/911 enabled/autofixed
213
223
elif error_code [:7 ] not in self .options .enabled_codes :
214
- return # pragma: no cover
215
- pos = self .get_metadata (PositionProvider , node ).start
224
+ return False # pragma: no cover
216
225
226
+ if self .is_noqa (node , error_code ):
227
+ return False
228
+
229
+ pos = self .get_metadata (PositionProvider , node ).start
217
230
self .__state .problems .append (
218
231
Error (
219
232
# 7 == len('TRIO...'), so alt messages raise the original code
@@ -224,13 +237,60 @@ def error(
224
237
* args ,
225
238
)
226
239
)
240
+ return True
227
241
228
- def should_autofix (self , code : str | None = None ):
242
+ def should_autofix (self , node : cst . CSTNode , code : str | None = None ) -> bool :
229
243
if code is None :
230
244
assert len (self .error_codes ) == 1
231
245
code = next (iter (self .error_codes ))
246
+ if self .is_noqa (node , code ):
247
+ return False
232
248
return code in self .options .autofix_codes
233
249
250
+ # These must not be overridden without calling super() on them.
251
+ # TODO: find a good way of enforcing that statically, or add a test param that
252
+ # all errors work with noqa, or do fancy metaclass stuff
253
+ # https://stackoverflow.com/questions/75033760/require-overriding-method-to-call-super
254
+ # or just #YOLO
255
+
256
+ def visit_SimpleStatementLine (self , node : cst .SimpleStatementLine ):
257
+ super ().visit_SimpleStatementLine (node )
258
+ self .save_state (node , "noqas" )
259
+ self .parse_noqa (node .trailing_whitespace .comment )
260
+
261
+ def leave_SimpleStatementLine (
262
+ self ,
263
+ original_node : cst .SimpleStatementLine ,
264
+ updated_node : cst .SimpleStatementLine ,
265
+ ):
266
+ super_updated_node = super ().leave_SimpleStatementLine (
267
+ original_node , updated_node
268
+ )
269
+ self .restore_state (original_node )
270
+ return super_updated_node # noqa: R504
271
+
272
+ def visit_SimpleStatementSuite (self , node : cst .SimpleStatementSuite ):
273
+ self .save_state (node , "noqas" )
274
+ self .parse_noqa (node .trailing_whitespace .comment )
275
+
276
+ def leave_SimpleStatementSuite (
277
+ self ,
278
+ original_node : cst .SimpleStatementSuite ,
279
+ updated_node : cst .SimpleStatementSuite ,
280
+ ):
281
+ self .restore_state (original_node )
282
+ return updated_node
283
+
284
+ def visit_IndentedBlock (self , node : cst .IndentedBlock ):
285
+ self .save_state (node , "noqas" )
286
+ self .parse_noqa (node .header .comment )
287
+
288
+ def leave_IndentedBlock (
289
+ self , original_node : cst .IndentedBlock , updated_node : cst .IndentedBlock
290
+ ):
291
+ self .restore_state (original_node )
292
+ return updated_node
293
+
234
294
@property
235
295
def library (self ) -> tuple [str , ...]:
236
296
return self .__state .library if self .__state .library else ("trio" ,)
@@ -240,3 +300,56 @@ def library(self) -> tuple[str, ...]:
240
300
def add_library (self , name : str ) -> None :
241
301
if name not in self .__state .library :
242
302
self .__state .library = self .__state .library + (name ,)
303
+
304
+ def parse_noqa (self , node : cst .Comment | None ):
305
+ if not node :
306
+ return
307
+ noqa_match = _find_noqa (node .value )
308
+ if noqa_match is None :
309
+ return
310
+
311
+ codes_str = noqa_match .groupdict ()["codes" ]
312
+
313
+ line = self .get_metadata (PositionProvider , node ).start .line
314
+
315
+ assert (
316
+ line not in self .noqas
317
+ ), "it should not be possible to have two [noqa] comments on the same line"
318
+
319
+ # blanket noqa
320
+ if codes_str is None :
321
+ # this also includes a non-blanket noqa with a list of invalid codes
322
+ # so one should maybe instead specifically look for no `:`
323
+ self .noqas [line ] = set ()
324
+ return
325
+ # split string on ",", strip of whitespace, and save in set if non-empty
326
+ # TODO: Check that code exists
327
+ self .noqas [line ] = {
328
+ item_strip for item in codes_str .split ("," ) if (item_strip := item .strip ())
329
+ }
330
+
331
+
332
+ # taken from
333
+ # https://github.com/PyCQA/flake8/blob/d016204366a22d382b5b56dc14b6cbff28ce929e/src/flake8/defaults.py#L27
334
+ NOQA_INLINE_REGEXP = re .compile (
335
+ # We're looking for items that look like this:
336
+ # ``# noqa``
337
+ # ``# noqa: E123``
338
+ # ``# noqa: E123,W451,F921``
339
+ # ``# noqa:E123,W451,F921``
340
+ # ``# NoQA: E123,W451,F921``
341
+ # ``# NOQA: E123,W451,F921``
342
+ # ``# NOQA:E123,W451,F921``
343
+ # We do not want to capture the ``: `` that follows ``noqa``
344
+ # We do not care about the casing of ``noqa``
345
+ # We want a comma-separated list of errors
346
+ # upstream links to an old version on regex101
347
+ # https://regex101.com/r/4XUuax/5 full explanation of the regex
348
+ r"# noqa(?::[\s]?(?P<codes>([A-Z]+[0-9]+(?:[,\s]+)?)+))?" ,
349
+ re .IGNORECASE ,
350
+ )
351
+
352
+
353
+ @functools .lru_cache (maxsize = 512 )
354
+ def _find_noqa (physical_line : str ) -> Match [str ] | None :
355
+ return NOQA_INLINE_REGEXP .search (physical_line )
0 commit comments