2828
2929import ast
3030import difflib
31+ import functools
3132import collections
3233import distutils.sysconfig
3334import fnmatch
@@ -140,10 +141,11 @@ def star_import_usage_undefined_name(messages):
140141
141142
142143def 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+ }
147149
148150
149151def duplicate_key_line_numbers(messages, source):
@@ -372,10 +374,11 @@ def filter_code(source, additional_imports=None,
372374 marked_star_import_line_numbers = frozenset()
373375
374376 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+ )
377380 else:
378- marked_variable_line_numbers = frozenset()
381+ marked_variable_line_numbers = {}
379382
380383 if remove_duplicate_keys:
381384 marked_key_line_numbers = frozenset(
@@ -388,6 +391,7 @@ def filter_code(source, additional_imports=None,
388391 sio = io.StringIO(source)
389392 previous_line = ''
390393 for line_number, line in enumerate(sio.readlines(), start=1):
394+ unused_vars = marked_variable_line_numbers.get(line_number)
391395 if '#' in line:
392396 yield line
393397 elif line_number in marked_import_line_numbers:
@@ -397,8 +401,8 @@ def filter_code(source, additional_imports=None,
397401 remove_all_unused_imports=remove_all_unused_imports,
398402 imports=imports,
399403 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 )
402406 elif line_number in marked_key_line_numbers:
403407 yield filter_duplicate_key(line, line_messages[line_number],
404408 line_number, marked_key_line_numbers,
@@ -453,28 +457,65 @@ def filter_unused_import(line, unused_module, remove_all_unused_imports,
453457 get_line_ending(line))
454458
455459
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:
461464 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):
476468 return line
477469
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+
478519
479520def filter_duplicate_key(line, message, line_number, marked_line_numbers,
480521 source, previous_line=''):
0 commit comments