28
28
29
29
import ast
30
30
import difflib
31
+ import functools
31
32
import collections
32
33
import distutils .sysconfig
33
34
import fnmatch
@@ -140,10 +141,11 @@ def star_import_usage_undefined_name(messages):
140
141
141
142
142
143
def unused_variable_line_numbers (messages ):
143
- """Yield line numbers of unused variables."""
144
- for message in messages :
145
- if isinstance (message , pyflakes .messages .UnusedVariable ):
146
- yield message .lineno
144
+ """Dict of line numbers to unused variables."""
145
+ return {
146
+ m .lineno : frozenset (m .message_args )
147
+ for m in messages
148
+ }
147
149
148
150
149
151
def duplicate_key_line_numbers (messages , source ):
@@ -372,10 +374,11 @@ def filter_code(source, additional_imports=None,
372
374
marked_star_import_line_numbers = frozenset ()
373
375
374
376
if remove_unused_variables :
375
- marked_variable_line_numbers = frozenset (
376
- unused_variable_line_numbers (messages ))
377
+ marked_variable_line_numbers = (
378
+ unused_variable_line_numbers (messages )
379
+ )
377
380
else :
378
- marked_variable_line_numbers = frozenset ()
381
+ marked_variable_line_numbers = {}
379
382
380
383
if remove_duplicate_keys :
381
384
marked_key_line_numbers = frozenset (
@@ -388,6 +391,7 @@ def filter_code(source, additional_imports=None,
388
391
sio = io .StringIO (source )
389
392
previous_line = ''
390
393
for line_number , line in enumerate (sio .readlines (), start = 1 ):
394
+ unused_vars = marked_variable_line_numbers .get (line_number )
391
395
if '#' in line :
392
396
yield line
393
397
elif line_number in marked_import_line_numbers :
@@ -397,8 +401,8 @@ def filter_code(source, additional_imports=None,
397
401
remove_all_unused_imports = remove_all_unused_imports ,
398
402
imports = imports ,
399
403
previous_line = previous_line )
400
- elif line_number in marked_variable_line_numbers :
401
- yield filter_unused_variable (line )
404
+ elif unused_vars :
405
+ yield filter_unused_variable (line , unused_vars )
402
406
elif line_number in marked_key_line_numbers :
403
407
yield filter_duplicate_key (line , line_messages [line_number ],
404
408
line_number , marked_key_line_numbers ,
@@ -453,28 +457,65 @@ def filter_unused_import(line, unused_module, remove_all_unused_imports,
453
457
get_line_ending (line ))
454
458
455
459
456
- def filter_unused_variable (line , previous_line = '' ):
457
- """Return line if used, otherwise return None."""
458
- if re .match (EXCEPT_REGEX , line ):
459
- return re .sub (r' as \w+:$' , ':' , line , count = 1 )
460
- elif multiline_statement (line , previous_line ):
460
+ def _remove_one_assignment_target (unused_vars , line ):
461
+ try :
462
+ parsed = ast .parse (line )
463
+ except SyntaxError :
461
464
return line
462
- elif line .count ('=' ) == 1 :
463
- split_line = line .split ('=' )
464
- assert len (split_line ) == 2
465
- value = split_line [1 ].lstrip ()
466
- if ',' in split_line [0 ]:
467
- return line
468
-
469
- if is_literal_or_name (value ):
470
- # Rather than removing the line, replace with it "pass" to avoid
471
- # a possible hanging block with no body.
472
- value = 'pass' + get_line_ending (line )
473
-
474
- return get_indentation (line ) + value
475
- else :
465
+
466
+ assignment = parsed .body [0 ]
467
+ if not isinstance (assignment , ast .Assign ):
476
468
return line
477
469
470
+ targets = assignment .targets
471
+ for target in assignment .targets :
472
+ if not isinstance (target , ast .Name ):
473
+ continue
474
+ name = target .id
475
+ if name not in unused_vars :
476
+ continue
477
+ offset = target .col_offset
478
+ return line [:offset ] + re .sub (
479
+ r'\A\s*' + re .escape (name ) + r'\s*=\s*' ,
480
+ '' , line [offset :],
481
+ count = 1 ,
482
+ )
483
+ return line
484
+
485
+
486
+ def _fix (fn , value ):
487
+ """
488
+ Apply fn to its output until it coverges
489
+ """
490
+ while True :
491
+ new_value = fn (value )
492
+ if new_value == value :
493
+ return new_value
494
+ value = new_value
495
+
496
+
497
+ def filter_unused_variable (line , unused_vars ):
498
+ """Return line if used, otherwise return None."""
499
+ if re .match (EXCEPT_REGEX , line ):
500
+ assert len (unused_vars ) == 1
501
+ unused_e , = unused_vars
502
+ return line .replace (
503
+ ' as {}:' .format (unused_e ),
504
+ ':' ,
505
+ 1 ,
506
+ )
507
+
508
+ indentation = get_indentation (line )
509
+ line = line [len (indentation ):]
510
+ remove = functools .partial (_remove_one_assignment_target , unused_vars )
511
+ line = _fix (remove , line )
512
+
513
+ if is_literal_or_name (line ):
514
+ # Rather than removing the line, replace with it "pass" to avoid
515
+ # a possible hanging block with no body.
516
+ return indentation + 'pass' + get_line_ending (line )
517
+ return indentation + line
518
+
478
519
479
520
def filter_duplicate_key (line , message , line_number , marked_line_numbers ,
480
521
source , previous_line = '' ):
0 commit comments