Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion apps/unused_code/unused_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,22 @@ def _build_fixture_param_pattern(function_name: str) -> str:
return rf"\<{function_name}\>[[:space:]]*[,:]"


def _build_keyword_unpacking_pattern(function_name: str) -> str:
r"""Build a portable regex to find keyword unpacking usage (**function_name()).

This pattern is designed to match patterns like **func_name() in function calls or dictionaries.
Uses word boundary semantics based on the detected grep engine.
- PCRE (-P): \*\*function_name\s*\(
- Basic (-G): \*\*function_name[[:space:]]*\(
"""
flag = _detect_supported_grep_flag()
if flag == "-P":
# Match ** followed by function name followed by optional whitespace and opening parenthesis
return rf"\*\*{function_name}\s*\("
# For -G (basic regex), escape the asterisks and use POSIX classes
return rf"\*\*{function_name}[[:space:]]*\("


def _is_pytest_mark_usefixtures_call(call_node: ast.Call) -> bool:
"""Check if an AST Call node represents pytest.mark.usefixtures(...)."""
if isinstance(call_node.func, ast.Attribute):
Expand Down Expand Up @@ -334,7 +350,7 @@ def _is_documentation_pattern(line: str, function_name: str) -> bool:

# Pattern 4: Check for common docstring patterns
# Lines that start with common documentation patterns
doc_starters = ['"""', "'''", "# ", "## ", "### ", "*", "-", "•"]
doc_starters = ['"""', "'''", "# ", "## ", "### ", "-", "•"]
if any(stripped_line.startswith(starter) for starter in doc_starters):
if f"{function_name}(" in stripped_line:
return True
Expand Down Expand Up @@ -487,6 +503,30 @@ def process_file(py_file: str, func_ignore_prefix: list[str], file_ignore_list:
used = True
break

# If not found with general pattern, check for keyword unpacking usage (**func_name())
if not used:
for entry in _git_grep(
pattern=_build_keyword_unpacking_pattern(function_name=func.name), file_path=py_file
):
LOGGER.debug(f"Checking {entry} function: {func.name}")
parts = entry.split(":", 2)
if len(parts) != 3:
continue
_, _, _line = parts

# Filter out documentation patterns that aren't actual function calls
if _is_documentation_pattern(line=_line, function_name=func.name):
LOGGER.debug(f"Skipping doc pattern {entry} function: {func.name}")
continue

# Ignore commented lines (full line or inline)
if _line.strip().startswith("#"):
continue

# If we find keyword unpacking usage, mark as used
used = True
break

# If not found and it's a pytest fixture, check all fixture usage patterns
if not used and is_pytest_fixture(func=func):
patterns: list[tuple[str, Callable[..., bool] | None]] = [
Expand Down
155 changes: 155 additions & 0 deletions tests/unused_code/test_unused_code_keyword_unpacking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import textwrap

from simple_logger.logger import get_logger

from apps.unused_code.unused_code import get_unused_functions, process_file
from tests.utils import get_cli_runner

LOGGER = get_logger(name=__name__)


def test_keyword_unpacking_usage_is_detected(mocker, tmp_path):
"""Test that functions used in **function_name() patterns are detected as used."""
# Create a temporary python file with a function that would be unused without keyword unpacking
py_file = tmp_path / "tmp_keyword_unpacking.py"
py_file.write_text(
textwrap.dedent(
"""
def get_config():
return {"key": "value"}
"""
)
)

# Mock git grep to simulate the detection
def _mock_grep(pattern: str, **kwargs):
# Regular usage pattern - no matches found
if pattern == r"\bget_config\b":
return []
# Keyword unpacking pattern - match found
elif pattern == r"\*\*get_config\s*\(":
return [f"{py_file.as_posix()}:10: result = some_function(**get_config())"]
return []

mocker.patch("apps.unused_code.unused_code._git_grep", side_effect=_mock_grep)

result = process_file(py_file=str(py_file), func_ignore_prefix=[], file_ignore_list=[])
assert result == ""


def test_keyword_unpacking_in_function_definition_is_detected(mocker, tmp_path):
"""Test that functions used in keyword unpacking within function definitions are detected as used."""
py_file = tmp_path / "tmp_keyword_unpacking_def.py"
py_file.write_text(
textwrap.dedent(
"""
def helper_function():
return {"key": "value"}
"""
)
)

def _mock_grep(pattern: str, **kwargs):
if pattern == r"\bhelper_function\b":
return []
elif pattern == r"\*\*helper_function\s*\(":
# Return a function definition that USES helper_function - this should be detected
return [f"{py_file.as_posix()}:5:def target_function(**helper_function()):"]
return []

mocker.patch("apps.unused_code.unused_code._git_grep", side_effect=_mock_grep)

result = process_file(py_file=str(py_file), func_ignore_prefix=[], file_ignore_list=[])
assert result == ""


def test_keyword_unpacking_ignores_comments(mocker, tmp_path):
"""Test that commented keyword unpacking usage is properly ignored."""
py_file = tmp_path / "tmp_keyword_unpacking_comment.py"
py_file.write_text(
textwrap.dedent(
"""
def config_helper():
return {"setting": "value"}
"""
)
)

def _mock_grep(pattern: str, **kwargs):
if pattern == r"\bconfig_helper\b":
return []
elif pattern == r"\*\*config_helper\s*\(":
# Return a commented usage which should be ignored
return [f"{py_file.as_posix()}:6: # result = setup(**config_helper())"]
return []

mocker.patch("apps.unused_code.unused_code._git_grep", side_effect=_mock_grep)

result = process_file(py_file=str(py_file), func_ignore_prefix=[], file_ignore_list=[])
assert "Is not used anywhere in the code." in result


def test_keyword_unpacking_ignores_documentation_patterns(mocker, tmp_path):
"""Test that documentation patterns with keyword unpacking are properly ignored."""
py_file = tmp_path / "tmp_keyword_unpacking_doc.py"
py_file.write_text(
textwrap.dedent(
'''
def doc_function():
"""
Example: setup(**doc_function())
This function returns configuration data.
"""
return {"config": "value"}
'''
)
)

def _mock_grep(pattern: str, **kwargs):
if pattern == r"\bdoc_function\b":
return []
elif pattern == r"\*\*doc_function\s*\(":
# Return documentation pattern which should be ignored
return [f"{py_file.as_posix()}:4: Example: setup(**doc_function())"]
return []

mocker.patch("apps.unused_code.unused_code._git_grep", side_effect=_mock_grep)

result = process_file(py_file=str(py_file), func_ignore_prefix=[], file_ignore_list=[])
assert "Is not used anywhere in the code." in result


def test_keyword_unpacking_with_cli(tmp_path):
"""Test keyword unpacking detection through the CLI interface."""
py_file = tmp_path / "test_keyword_unpacking.py"
py_file.write_text(
textwrap.dedent(
"""
def get_config():
return {"database_url": "localhost", "debug": True}

def get_defaults():
return {"timeout": 30, "retries": 3}

def unused_function():
return "This should be flagged as unused"

def main():
config = {
**get_config(),
"extra": "value"
}
result = setup(**get_defaults())
return result
"""
)
)

result = get_cli_runner().invoke(
get_unused_functions,
["--file-path", str(py_file)],
)
assert result.exit_code == 1
assert "get_config" not in result.output
assert "get_defaults" not in result.output
assert "Is not used anywhere in the code." in result.output