Skip to content

Commit 22d793b

Browse files
committed
use ast to remove unused assignments Fixes #48
1 parent f30bec9 commit 22d793b

File tree

2 files changed

+92
-42
lines changed

2 files changed

+92
-42
lines changed

autoflake.py

+70-27
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
import ast
3030
import difflib
31+
import functools
3132
import collections
3233
import distutils.sysconfig
3334
import fnmatch
@@ -140,10 +141,11 @@ def star_import_usage_undefined_name(messages):
140141

141142

142143
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+
}
147149

148150

149151
def 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,67 @@ 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=''):
460+
def _remove_one_assignment_target(unused_vars, line):
461+
try:
462+
parsed = ast.parse(line)
463+
except SyntaxError:
464+
return line
465+
466+
assignment = parsed.body[0]
467+
if not isinstance(assignment, ast.Assign):
468+
return line
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):
457498
"""Return line if used, otherwise return None."""
458499
if re.match(EXCEPT_REGEX, line):
459-
return re.sub(r' as \w+:$', ':', line, count=1)
460-
elif multiline_statement(line, previous_line):
461-
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:
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+
if multiline_statement(line, ''):
476508
return line
477509

510+
indentation = get_indentation(line)
511+
line = line[len(indentation):]
512+
remove = functools.partial(_remove_one_assignment_target, unused_vars)
513+
line = _fix(remove, line)
514+
515+
if is_literal_or_name(line):
516+
# Rather than removing the line, replace with it "pass" to avoid
517+
# a possible hanging block with no body.
518+
return indentation + 'pass' + get_line_ending(line)
519+
return indentation + line
520+
478521

479522
def filter_duplicate_key(line, message, line_number, marked_line_numbers,
480523
source, previous_line=''):

test_autoflake.py

+22-15
Original file line numberDiff line numberDiff line change
@@ -109,50 +109,57 @@ def test_filter_star_import(self):
109109

110110
def test_filter_unused_variable(self):
111111
self.assertEqual('foo()',
112-
autoflake.filter_unused_variable('x = foo()'))
113-
self.assertEqual('foo(k=None)',
114-
autoflake.filter_unused_variable('x = foo(k=None)'))
112+
autoflake.filter_unused_variable('x = foo()', 'x'))
115113

116114
self.assertEqual(' foo()',
117-
autoflake.filter_unused_variable(' x = foo()'))
115+
autoflake.filter_unused_variable(' x = foo()', 'x'))
116+
117+
def test_filter_unused_variable_kwarg(self):
118+
self.assertEqual('foo(k=None)',
119+
autoflake.filter_unused_variable('x = foo(k=None)', 'x'))
118120

119121
def test_filter_unused_variable_with_literal_or_name(self):
120122
self.assertEqual('pass',
121-
autoflake.filter_unused_variable('x = 1'))
123+
autoflake.filter_unused_variable('x = 1', 'x'))
122124

123125
self.assertEqual('pass',
124-
autoflake.filter_unused_variable('x = y'))
126+
autoflake.filter_unused_variable('x = y', 'x'))
125127

126128
self.assertEqual('pass',
127-
autoflake.filter_unused_variable('x = {}'))
129+
autoflake.filter_unused_variable('x = {}', 'x'))
128130

129131
def test_filter_unused_variable_with_basic_data_structures(self):
130132
self.assertEqual('pass',
131-
autoflake.filter_unused_variable('x = dict()'))
133+
autoflake.filter_unused_variable('x = dict()', 'x'))
132134

133135
self.assertEqual('pass',
134-
autoflake.filter_unused_variable('x = list()'))
136+
autoflake.filter_unused_variable('x = list()', 'x'))
135137

136138
self.assertEqual('pass',
137-
autoflake.filter_unused_variable('x = set()'))
139+
autoflake.filter_unused_variable('x = set()', 'x'))
138140

139141
def test_filter_unused_variable_should_ignore_multiline(self):
140142
self.assertEqual('x = foo()\\',
141-
autoflake.filter_unused_variable('x = foo()\\'))
143+
autoflake.filter_unused_variable('x = foo()\\', 'x'))
142144

143145
def test_filter_unused_variable_should_multiple_assignments(self):
144-
self.assertEqual('x = y = foo()',
145-
autoflake.filter_unused_variable('x = y = foo()'))
146+
self.assertEqual('y = foo()',
147+
autoflake.filter_unused_variable('x = y = foo()', 'x'))
148+
self.assertEqual('x = foo()',
149+
autoflake.filter_unused_variable('x = y = foo()', 'y'))
150+
self.assertEqual('foo()',
151+
autoflake.filter_unused_variable('x = y = foo()', 'xy'))
152+
146153

147154
def test_filter_unused_variable_with_exception(self):
148155
self.assertEqual(
149156
'except Exception:',
150-
autoflake.filter_unused_variable('except Exception as exception:'))
157+
autoflake.filter_unused_variable('except Exception as exception:', {'exception'}))
151158

152159
self.assertEqual(
153160
'except (ImportError, ValueError):',
154161
autoflake.filter_unused_variable(
155-
'except (ImportError, ValueError) as foo:'))
162+
'except (ImportError, ValueError) as foo:', {'foo'}))
156163

157164
def test_filter_code(self):
158165
self.assertEqual(

0 commit comments

Comments
 (0)