Skip to content

Commit

Permalink
Add partial support for unused import removal
Browse files Browse the repository at this point in the history
  • Loading branch information
albertas committed Apr 16, 2024
1 parent f704ca3 commit 3f09116
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 3 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ code base is implemented in.
- [ ] Check if file is still valid/parsable after automatic fixing, if not: halt the change and report error.

## Release notes
- v2.3.1:
- Added support for automatic removal of imports.
- v2.3.0:
- Add `--dry` option.
- Update error codes to use DCXX format instead of DCXXX.
19 changes: 18 additions & 1 deletion deadcode/actions/remove_file_parts_from_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ def remove_as_from_end(line: str) -> str:
return line


def remove_comma_from_begining(line: str) -> str:
if not line.lstrip().startswith(','):
return line

return line.lstrip()[1:].lstrip()


def remove_file_parts_from_content(content_lines: List[str], unused_file_parts: List[Part]) -> List[str]:
""" """
# How should move through the lines of content?
Expand Down Expand Up @@ -63,10 +70,14 @@ def remove_file_parts_from_content(content_lines: List[str], unused_file_parts:
indentation_of_first_removed_line = get_indentation(line)

if from_line == to_line:
# TODO: this check is a workaround for an assignment expression.
# Clean-ups have to be applied based on removable expression type.
if (line[:from_col] + line[to_col:]).strip().startswith('='):
line = line[:from_col]
else:
line = remove_as_from_end(line[:from_col]) + line[to_col:]
# TODO: should apply `as` removal rule only for particular expression type only
# as well as comma removal.
line = remove_as_from_end(line[:from_col]) + remove_comma_from_begining(line[to_col:])

unused_part_index += 1

Expand All @@ -87,6 +98,12 @@ def remove_file_parts_from_content(content_lines: List[str], unused_file_parts:
# Is it last line, from the block, which have to be ignored?
elif current_lineno == to_line:
line = line[to_col:]

# Remove trailing comma, if we have removed something.
# TODO: this should be applied only for specific expression types only.
if line.lstrip().startswith(","):
line = line.lstrip()[1:].lstrip()

unused_part_index += 1
# TODO: Add tests for case, when comments are added at the start or end of the removed block
if line.strip() and not line.startswith('#'):
Expand Down
7 changes: 7 additions & 0 deletions deadcode/visitor/code_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class CodeItem: # TODO: This should also be a dataclass, because hash and tuple
'type_',
'filename',
'code_parts',
'expression_code_parts',
'scope',
'inherits_from',
'name_line',
Expand All @@ -66,6 +67,7 @@ def __init__(
# first_column: int = 0,
# last_column: Optional[int] = None,
code_parts: Optional[List[Part]] = None, # TODO: I should use a dataclass instead of a tuple for Part.
expression_code_parts: Optional[List[Part]] = None,
scope: Optional[str] = None,
inherits_from: Optional[List[str]] = None,
name_line: Optional[int] = None,
Expand All @@ -85,6 +87,11 @@ def __init__(
else:
self.code_parts = code_parts

if expression_code_parts is None:
self.expression_code_parts = []
else:
self.expression_code_parts = expression_code_parts

# if first_lineno is not None:
# pass

Expand Down
20 changes: 18 additions & 2 deletions deadcode/visitor/dead_code_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,8 @@ def _add_aliases(self, node: Union[ast.Import, ast.ImportFrom]) -> None:
self._define(
self.defined_imports,
alias or name,
node,
first_node=name_and_alias,
expression_node=node,
ignore=_ignore_import,
)
if alias is not None:
Expand Down Expand Up @@ -234,6 +235,7 @@ def _define(
name: str,
first_node: ast.AST,
last_node: Optional[ast.AST] = None,
expression_node: Optional[ast.AST] = None,
message: str = '',
ignore: Optional[Callable[[Path, str], bool]] = None,
) -> None:
Expand All @@ -255,8 +257,8 @@ def ignored(lineno: int, type_: UnusedCodeType) -> bool:

last_node = last_node or first_node
type_: UnusedCodeType = collection.type_ # type: ignore
first_lineno = lines.get_first_line_number(first_node)

first_lineno = lines.get_first_line_number(first_node)
last_lineno = lines.get_last_line_number(last_node)

inherits_from = getattr(first_node, 'inherits_from', None)
Expand All @@ -271,6 +273,20 @@ def ignored(lineno: int, type_: UnusedCodeType) -> bool:
message=message,
inherits_from=inherits_from,
)

# TODO: Remaining import expressions has to be removed, when all imported
# names get removed. Pass whole import expression parts and evaluate if its still
# empty after making code adjustments.
# This feature is not yet implemented.
if expression_node:
expression_first_lineno = lines.get_first_line_number(expression_node)
expression_last_lineno = lines.get_last_line_number(expression_node)
code_item.expression_code_parts = [Part(
expression_first_lineno,
expression_last_lineno,
expression_node.col_offset,
expression_node.end_col_offset or 0)]

self.scopes.add(code_item)

if ignored(first_lineno, type_=type_):
Expand Down
59 changes: 59 additions & 0 deletions tests/fix/test_unused_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""
Test unused import detection and removal.
"""

from unittest import skip

from deadcode.cli import main
from deadcode.utils.base_test_case import BaseTestCase
from deadcode.utils.fix_indent import fix_indent


class TestAssignmentExpressionRemoval(BaseTestCase):
def test_variable(self):
self.files = {
'file2.py': """
from file1 import (
foo,
bar,
xyz
)
from file1 import foo
def fn():
pass
fn()
"""
}

unused_names = main('file2.py --no-color --fix -v'.split())

self.assertEqual(
unused_names,
fix_indent("""\
file2.py:2:4: DC07 Import `foo` is never used
file2.py:3:4: DC07 Import `bar` is never used
file2.py:4:4: DC07 Import `xyz` is never used
file2.py:7:18: DC07 Import `foo` is never used
Removed 4 unused code items!""")
)

# TODO: empty imports statements should be removed as well.
self.assertFiles(
{
'file2.py': """
from file1 import (
)
from file1 import
def fn():
pass
fn()
"""
}
)

0 comments on commit 3f09116

Please sign in to comment.