Skip to content

Add ability to skip a single function def, fix relative paths for exclude-files in unused-code#156

Closed
lugi0 wants to merge 4 commits into
RedHatQE:mainfrom
lugi0:main
Closed

Add ability to skip a single function def, fix relative paths for exclude-files in unused-code#156
lugi0 wants to merge 4 commits into
RedHatQE:mainfrom
lugi0:main

Conversation

@lugi0
Copy link
Copy Markdown

@lugi0 lugi0 commented Mar 3, 2025

Currently unused-code does not provide the ability to skip single function definitions, hence I've added the ability to skip via a specific comment added before the function definition (# skip-unused-code).
I've also fixed the behaviour of --exclude-files, which currently only works with file names and excludes all files of a given name in a project (e.g. --exclude-files conftest.py would exclude all conftest.py files in your project). Passing in relative paths to a specific file you want to exclude would be ignored, e.g. --exclude-files sub/dir/confest.py would be ignored and the specific sub/dir/conftest.py would be checked for unused code.
It now only excludes the file being specified, e.g. --exclude-files conftest.py would exclude conftest.py in the project root while --exclude-files sub/dir/confest.py (correctly) only excludes sub/dir/confest.py

Summary by CodeRabbit

  • New Features

    • Introduced a mechanism that lets users exclude specific functions from being checked for unused code by using the # skip-unused-code comment.
    • Enhanced file scanning with a two-stage evaluation and refined path-based checks, ensuring more accurate and targeted processing.
    • Added example functions in the documentation to illustrate the new exclusion feature.
  • Bug Fixes

    • Improved test coverage with new scenarios and refined assertions for better validation of functionality.

Signed-off-by: lugi0 <lgiorgi@redhat.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 3, 2025

Walkthrough

The changes introduce a new function, extract_inline_function_comments, in apps/unused_code/unused_code.py, which extracts inline comments associated with function definitions that match a specific comment string, SKIP_COMMENT. The process_file function is modified to read the source code twice: once for parsing the AST and again for extracting comments. The logic for skipping functions and files has been updated to check the relative path against a file_ignore_list. Additionally, several test cases are added or modified to enhance coverage and validation of the new functionality.

Changes

File(s) Change Summary
apps/unused_code/unused_code.py - Added extract_inline_function_comments function to extract comments associated with function definitions.
- Defined constant SKIP_COMMENT with the value "# skip-unused-code".
- Modified process_file to read source code twice for AST parsing and comment extraction, and updated logic for skipping functions and files.
apps/unused_code/README.md - Added a new section explaining how to exclude functions from unused code checks using the # skip-unused-code comment, including examples.
tests/unused_code/test_unused_code.py - Updated test_unused_code_file_list to specify the full path for the excluded file.
- Added test_unused_code_wrong_file_list to test behavior with incorrect file exclusions.
- Modified assertions in test_unused_code_function_list_exclude_all and test_unused_code_function_list_exclude for more accurate validation.
- Added test_skip_comment to validate functionality with skip comments.
tests/unused_code/unused_code_file_for_test.py - Added new functions: foo, bar, check_me, and check_me_too, all marked with # skip-unused-code. Imported typing for type annotations.
pyproject.toml - Updated the exclude list in the [tool.ruff.format] section to add unused_code_file_for_test.py, preventing it from being included in the linting process.

Suggested labels

verified, can-be-merged, approved-myakove, commented-coderabbitai[bot]

Suggested reviewers

  • myakove
  • dbasunag

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@redhat-qe-bot
Copy link
Copy Markdown
Collaborator

Report bugs in Issues

The following are automatically added:

  • Add reviewers from OWNER file (in the root of the repository) under reviewers section.
  • Set PR size label.
  • New issue is created for the PR. (Closed when PR is merged/closed)
  • Run pre-commit if .pre-commit-config.yaml exists in the repo.

Available user actions:

  • To mark PR as WIP comment /wip to the PR, To remove it from the PR comment /wip cancel to the PR.
  • To block merging of PR comment /hold, To un-block merging of PR comment /hold cancel.
  • To mark PR as verified comment /verified to the PR, to un-verify comment /verified cancel to the PR.
    verified label removed on each new commit push.
  • To cherry pick a merged PR comment /cherry-pick <target branch to cherry-pick to> in the PR.
    • Multiple target branches can be cherry-picked, separated by spaces. (/cherry-pick branch1 branch2)
    • Cherry-pick will be started when PR is merged
  • To build and push container image command /build-and-push-container in the PR (tag will be the PR number).
    • You can add extra args to the Podman build command
      • Example: /build-and-push-container --build-arg OPENSHIFT_PYTHON_WRAPPER_COMMIT=<commit_hash>
  • To add a label by comment use /<label name>, to remove, use /<label name> cancel
  • To assign reviewers based on OWNERS file use /assign-reviewers
  • To check if PR can be merged use /check-can-merge
  • to assign reviewer to PR use /assign-reviewer @<reviewer>
Supported /retest check runs
  • /retest tox: Retest tox
  • /retest python-module-install: Retest python-module-install
  • /retest all: Retest all
Supported labels
  • hold
  • verified
  • wip
  • lgtm

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
apps/unused_code/unused_code.py (2)

111-112: File is read twice, which may impact performance.

Reading the file twice (once for AST parsing and once for comment extraction) may impact performance on large files or codebases. Consider reading the file once and reusing the content.

with open(py_file) as fd:
-    tree = ast.parse(source=fd.read())
+    source_code = fd.read()
+    tree = ast.parse(source=source_code)

-with open(py_file) as fd:
-    comments = extract_function_comments(source_code=fd.read())
+comments = extract_function_comments(source_code=source_code)

149-165: Consider adding documentation about new skip comment feature in CLI help.

The CLI help text for existing options is detailed, but there's no mention of the new skip comment feature. Adding information about this feature would improve discoverability.

Consider adding a note about the new skip comment feature to the CLI help text, e.g.:

@click.option(
    "--exclude-function-prefixes",
    help="Provide a comma-separated string or list of function prefixes to exclude",
    type=ListParamType(),
)
+@click.help_option('--help', '-h', help="Show this message and exit. Note: You can skip individual functions by adding '# skip-unused-code' comment before the function definition.")
@click.option("--verbose", "-v", default=False, is_flag=True)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 678ed22 and ff18c6c.

📒 Files selected for processing (1)
  • apps/unused_code/unused_code.py (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: tox
  • GitHub Check: python-module-install
🔇 Additional comments (4)
apps/unused_code/unused_code.py (4)

8-10: Added necessary imports for tokenization.

The new imports are correctly added to support the new function that extracts comments from source code.


19-19: Good constant definition.

Defining a constant for the skip comment makes the code more maintainable and prevents magic strings throughout the codebase.


104-104: Fixed file exclusion to use relative paths.

The change from using basename to relative path is correct and aligns with the PR objective to fix relative paths for exclude-files. This allows for more targeted exclusion of files.


22-26: Clear and effective docstring.

The docstring is clear and effectively explains the purpose of the function, along with an example of how to use the skip comment.

Comment thread apps/unused_code/unused_code.py Outdated
Comment on lines +115 to +117
if SKIP_COMMENT in comments[func.name]:
LOGGER.debug(f"Skipping function: {func.name} due to comment")
continue
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Need to handle potential KeyError in comments dictionary.

The code doesn't check if the function name exists in the comments dictionary before accessing it, which could raise a KeyError if a function doesn't have any associated comments.

for func in _iter_functions(tree=tree):
-    if SKIP_COMMENT in comments[func.name]:
+    if func.name in comments and SKIP_COMMENT in comments[func.name]:
        LOGGER.debug(f"Skipping function: {func.name} due to comment")
        continue
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if SKIP_COMMENT in comments[func.name]:
LOGGER.debug(f"Skipping function: {func.name} due to comment")
continue
for func in _iter_functions(tree=tree):
if func.name in comments and SKIP_COMMENT in comments[func.name]:
LOGGER.debug(f"Skipping function: {func.name} due to comment")
continue

Comment thread apps/unused_code/unused_code.py Outdated
Comment on lines +22 to +64
def extract_function_comments(source_code: str) -> dict[str, list[str]]:
"""
Finds the comments preceding function definitions, which can be used to skip single functions with a specific comment,
i.e. `# skip-unused-code`
"""
# Create an AST from the source code
tree = ast.parse(source_code)

# Initialize a dictionary to hold comments for each function
function_comments: dict[str, list[str]] = {}

# Tokenize the source code to find comments
tokens = tokenize.generate_tokens(StringIO(source_code).readline)

# To store the comments for each function
function_comments = {}
current_function = None
comments: list[str] = []
function_start_line = None

# Process the tokens and extract comments
for token in tokens:
if token.type == tokenize.COMMENT:
# Associate the comment with the current function, if any
if current_function is not None:
comments.append(token.string)

# Detect the start of a new function definition
elif token.type == tokenize.NAME and token.string == "def":
# Record the function's starting line
function_start_line = token.start[0]

elif token.type == tokenize.NAME and token.string != "def":
if function_start_line is not None:
# Find the function name in the AST node
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef) and node.lineno == function_start_line:
current_function = node.name
function_comments[current_function] = comments
function_start_line = None
comments = []

return function_comments
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Function comment extraction implementation needs improvement.

The function has a few issues:

  1. function_comments is defined twice (lines 31 and 37)
  2. The function doesn't handle edge cases well - like when a function name doesn't exist in comments
  3. There's no error handling if parsing fails

Here's a suggested improvement:

def extract_function_comments(source_code: str) -> dict[str, list[str]]:
    """
    Finds the comments preceding function definitions, which can be used to skip single functions with a specific comment,
    i.e. `# skip-unused-code`
    """
    # Create an AST from the source code
    tree = ast.parse(source_code)

    # Initialize a dictionary to hold comments for each function
-   function_comments: dict[str, list[str]] = {}
+   function_comments: dict[str, list[str]] = {}
    
    # Tokenize the source code to find comments
    tokens = tokenize.generate_tokens(StringIO(source_code).readline)

    # To store the comments for each function
-   function_comments = {}
    current_function = None
    comments: list[str] = []
    function_start_line = None

    # Process the tokens and extract comments
    for token in tokens:
        if token.type == tokenize.COMMENT:
            # Associate the comment with the current function, if any
            if current_function is not None:
                comments.append(token.string)

        # Detect the start of a new function definition
        elif token.type == tokenize.NAME and token.string == "def":
            # Record the function's starting line
            function_start_line = token.start[0]

        elif token.type == tokenize.NAME and token.string != "def":
            if function_start_line is not None:
                # Find the function name in the AST node
                for node in ast.walk(tree):
                    if isinstance(node, ast.FunctionDef) and node.lineno == function_start_line:
                        current_function = node.name
                        function_comments[current_function] = comments
                        function_start_line = None
                        comments = []

    return function_comments
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def extract_function_comments(source_code: str) -> dict[str, list[str]]:
"""
Finds the comments preceding function definitions, which can be used to skip single functions with a specific comment,
i.e. `# skip-unused-code`
"""
# Create an AST from the source code
tree = ast.parse(source_code)
# Initialize a dictionary to hold comments for each function
function_comments: dict[str, list[str]] = {}
# Tokenize the source code to find comments
tokens = tokenize.generate_tokens(StringIO(source_code).readline)
# To store the comments for each function
function_comments = {}
current_function = None
comments: list[str] = []
function_start_line = None
# Process the tokens and extract comments
for token in tokens:
if token.type == tokenize.COMMENT:
# Associate the comment with the current function, if any
if current_function is not None:
comments.append(token.string)
# Detect the start of a new function definition
elif token.type == tokenize.NAME and token.string == "def":
# Record the function's starting line
function_start_line = token.start[0]
elif token.type == tokenize.NAME and token.string != "def":
if function_start_line is not None:
# Find the function name in the AST node
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef) and node.lineno == function_start_line:
current_function = node.name
function_comments[current_function] = comments
function_start_line = None
comments = []
return function_comments
def extract_function_comments(source_code: str) -> dict[str, list[str]]:
"""
Finds the comments preceding function definitions, which can be used to skip single functions with a specific comment,
i.e. `# skip-unused-code`
"""
# Create an AST from the source code
tree = ast.parse(source_code)
# Initialize a dictionary to hold comments for each function
function_comments: dict[str, list[str]] = {}
# Tokenize the source code to find comments
tokens = tokenize.generate_tokens(StringIO(source_code).readline)
# To store the comments for each function
current_function = None
comments: list[str] = []
function_start_line = None
# Process the tokens and extract comments
for token in tokens:
if token.type == tokenize.COMMENT:
# Associate the comment with the current function, if any
if current_function is not None:
comments.append(token.string)
# Detect the start of a new function definition
elif token.type == tokenize.NAME and token.string == "def":
# Record the function's starting line
function_start_line = token.start[0]
elif token.type == tokenize.NAME and token.string != "def":
if function_start_line is not None:
# Find the function name in the AST node
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef) and node.lineno == function_start_line:
current_function = node.name
function_comments[current_function] = comments
function_start_line = None
comments = []
return function_comments

Comment thread apps/unused_code/unused_code.py
Copy link
Copy Markdown
Collaborator

@myakove myakove left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add tests.

Comment thread apps/unused_code/unused_code.py
Comment thread apps/unused_code/unused_code.py Outdated
Comment thread apps/unused_code/unused_code.py Outdated
Comment thread apps/unused_code/unused_code.py
Comment thread apps/unused_code/unused_code.py Outdated
Signed-off-by: lugi0 <lgiorgi@redhat.com>
@lugi0
Copy link
Copy Markdown
Author

lugi0 commented Mar 4, 2025

@myakove @dbasunag addressed the comments. I've updated the logic to only look for inline comments on function definitions equal to SKIP_COMMENT, which I'm open to updating if you don't like as the default comment to use.

I've also slightly tweaked the logic of process_file, which now treats functions that are only called from an assert statement as unused (this is opinionated, I know, but otherwise the included tests cannot work). It will now also report all unused functions of a given file rather than return only the first one found.

I've also updated and added test cases to check the updated logic all around.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
apps/unused_code/unused_code.py (1)

124-126: ⚠️ Potential issue

Fix potential KeyError when checking comments.

The code doesn't check if the function name exists in the comments dictionary before accessing it, which could raise a KeyError if a function doesn't have any associated comments.

-        if func.name in comments.keys():
+        if func.name in comments:
🧹 Nitpick comments (7)
apps/unused_code/README.md (1)

37-40: Add a comma for better readability.

There should be a comma after "repository" to improve readability.

-To skip single functions in your target repository you can add an inline comment to the function definition. The comment should match `# skip-unused-code`
+To skip single functions in your target repository, you can add an inline comment to the function definition. The comment should match `# skip-unused-code`
🧰 Tools
🪛 LanguageTool

[uncategorized] ~39-~39: Possible missing comma found.
Context: ...To skip single functions in your target repository you can add an inline comment to the fu...

(AI_HYDRA_LEO_MISSING_COMMA)

apps/unused_code/unused_code.py (4)

22-26: Improve docstring clarity.

The docstring could be more specific about what this function returns and how it's used.

-    """
-    Finds *only* inline comments for function definition that match `SKIP_COMMENT` and returns them
-    """
+    """
+    Finds *only* inline comments for function definitions that match `SKIP_COMMENT`.
+    
+    Returns:
+        dict[str, list[str]]: A dictionary mapping function names to a list of their inline comments
+        that match the SKIP_COMMENT pattern.
+    """

34-71: Handle potential errors in comment extraction.

The function doesn't handle potential parsing errors and edge cases. If tokenization fails or if there are malformed Python files, this could raise exceptions.

Add error handling to make the function more robust:

def extract_inline_function_comments(source_code: str) -> dict[str, list[str]]:
    """
    Finds *only* inline comments for function definition that match `SKIP_COMMENT` and returns them
    """
    # Tokenize the source code to find comments
-   tokens = tokenize.generate_tokens(StringIO(source_code).readline)
+   try:
+       tokens = tokenize.generate_tokens(StringIO(source_code).readline)
+   except tokenize.TokenError:
+       LOGGER.warning("Failed to tokenize source code, skipping comment extraction")
+       return {}

    # To store the comments for each function
    prev_token = None
    comments = {}
    def_tok = False

    # Process the tokens and extract comments
-   for token in tokens:
+   try:
+       for token in tokens:
            tok_type, tok_string, _, _, _ = token

            # Detect the start of a new function definition
            if tok_type == tokenize.NAME and tok_string == "def":
                def_tok = True

            elif tok_type == tokenize.NAME and def_tok:
                # First "NAME" token after a "def" will be the function name
                prev_token = token
                def_tok = False

            elif tok_type == tokenize.NEWLINE and prev_token:
                # we found a function name and this is the first logical newline after it
                # if no comment has been found it means that anything that comes after could be within the function
                # or outside of it, which is outside the scope of what we are looking for. we can empty prev_token.
                # note that tokenize.NL would be a different (non-logical) newline, e.g. a multi-line function def
                # which is thus still handled correctly.
                # Not handling this here can cause comments outside the scope of the function to be mishandled, e.g.
                # ------------
                # def foo():
                #      pass
                #
                # # my-comment
                # def bar():
                # ------------
                # would return "# my-comment" as a foo() comment
                prev_token = None

            # If this is the comment we look for, and it comes after a function definition
            elif tok_type == tokenize.COMMENT and prev_token and tok_string == SKIP_COMMENT:
                LOGGER.debug(f"found comment for function def: {prev_token.line.strip()}")
                LOGGER.debug(f"comment is: {tok_string}")
                func_name = prev_token.string
                comments[func_name] = [tok_string]
                prev_token = None
+   except Exception as e:
+       LOGGER.warning(f"Error extracting comments: {e}")
+       return {}

    return comments

119-121: Consider reusing file content instead of re-reading.

The code reads the file twice: once for AST parsing and once for comment extraction. This could be inefficient for large files.

-    with open(py_file) as fd:
-        tree = ast.parse(source=fd.read())
-
-    with open(py_file) as fd:
-        comments = extract_inline_function_comments(source_code=fd.read())
+    with open(py_file) as fd:
+        source_code = fd.read()
+        
+    tree = ast.parse(source=source_code)
+    comments = extract_inline_function_comments(source_code=source_code)

148-151: Add a comment explaining the assert statement skip logic.

The code skips functions that are only called from test assert statements, but there's no comment explaining this logic.

            if _line.strip().startswith("assert"):
-                # if the function is only called from a test assert statement do not count it
+                # If the function is only called from a test assert statement, do not count it
+                # as being used, as it might only be referenced in tests and not actual code
                continue
tests/unused_code/test_unused_code.py (2)

35-40: Enhance test assertions with explanatory messages.

The test assertions would be clearer with explanatory messages to indicate what is being tested.

-    # No function def that starts with "unused_code_"
-    assert ":unused_code_" not in result.output
-    assert "check_me" in result.output
-    assert "check_me_too" in result.output
-    assert "foo" not in result.output
-    assert "bar" not in result.output
+    # No function def that starts with "unused_code_" should be in the output
+    assert ":unused_code_" not in result.output, "Should not include functions with 'unused_code_' prefix"
+    # Functions with skip comments should not be in the output
+    assert "check_me" in result.output, "Should include 'check_me' as it's not properly skipped"
+    assert "check_me_too" in result.output, "Should include 'check_me_too' as it may not be properly skipped"
+    assert "foo" not in result.output, "Should not include 'foo' as it has a skip comment"
+    assert "bar" not in result.output, "Should not include 'bar' as it has a skip comment"

52-62: Add explicit test for the skip comment functionality.

This test is good for verifying the skip comment functionality, but could be enhanced to explicitly check that the skipping is due to the comments.

Consider adding a more explicit test that verifies the debug output showing the skip comment message:

def test_skip_comment():
-    result = get_cli_runner().invoke(get_unused_functions)
+    result = get_cli_runner().invoke(get_unused_functions, '-v')
    LOGGER.info(f"Result output: {result.output}, exit code: {result.exit_code}, exceptions: {result.exception}")
    assert result.exit_code == 1
    assert "unused_code_check_fail" in result.output
    assert "unused_code_check_file" in result.output
    assert "check_me" in result.output
    assert "check_me_too" in result.output
    assert "foo" not in result.output
    assert "bar" not in result.output
+    # Verify debug messages indicating functions were skipped due to comments
+    assert "Skipping function due to comment: foo" in result.output
+    assert "Skipping function due to comment: bar" in result.output
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ff18c6c and 4bd8c31.

📒 Files selected for processing (4)
  • apps/unused_code/README.md (1 hunks)
  • apps/unused_code/unused_code.py (4 hunks)
  • tests/unused_code/test_unused_code.py (1 hunks)
  • tests/unused_code/unused_code_file_for_test.py (1 hunks)
🧰 Additional context used
🪛 LanguageTool
apps/unused_code/README.md

[uncategorized] ~39-~39: Possible missing comma found.
Context: ...To skip single functions in your target repository you can add an inline comment to the fu...

(AI_HYDRA_LEO_MISSING_COMMA)


[uncategorized] ~43-~43: Possible missing comma found.
Context: ...sed-code` ### Example: Given a target file main.py ```python def tmp1(): # skip-u...

(AI_HYDRA_LEO_MISSING_COMMA)

🪛 markdownlint-cli2 (0.17.2)
apps/unused_code/README.md

41-41: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: tox
  • GitHub Check: python-module-install
🔇 Additional comments (9)
apps/unused_code/README.md (1)

54-59: Great implementation of verbose output examples.

Including the actual debug log examples showing what users will see when using the feature is very helpful for understanding the functionality.

tests/unused_code/unused_code_file_for_test.py (3)

12-13: Well-formatted skip comment.

The skip comment is correctly formatted according to the implementation.


16-17: Good test case with type annotations.

This provides a good test case for functions with type annotations and multiple parameters.


25-27:

Details

❓ Verification inconclusive

Comment placement may cause issues.

The skip comment is placed on a separate line before the function definition, but the implementation in extract_inline_function_comments is designed to find inline comments that follow function definitions.

This comment placement might not work as expected with the current implementation.


🏁 Script executed:

#!/bin/bash
# Check if the function with comment on previous line gets skipped

# Run the tool in verbose mode and check if check_me_too is skipped
python -m apps.unused_code.unused_code -v | grep "check_me_too"

Length of output: 386


Attention: Verify Skip Comment Behavior & Resolve Dependency Issue

  • The tool’s current implementation in extract_inline_function_comments expects skip comments to be inline (immediately following a function definition), but the skip comment in tests/unused_code/unused_code_file_for_test.py (lines 25–27) is placed on the line preceding the function definition.
  • Note that the verification attempt failed due to a missing dependency (simple_logger), which prevented a proper test run of this scenario. This dependency issue must be resolved before the skip-comment behavior can be reliably verified.
  • Action Items:
    • Ensure the simple_logger module is available in the testing environment so the tool can run without errors.
    • Re-verify that the skip comment placement is handled as expected. If the current inline detection is intended, consider placing the skip comment inline or modifying the detection logic to support preceding-line comments.
apps/unused_code/unused_code.py (3)

19-19: Good use of a constant for the skip comment.

Defining the skip comment pattern as a constant makes it easier to maintain and update in the future.


111-114: Correctly implemented relative path check.

The updated code properly checks relative paths against the file_ignore_list, which addresses the PR objective to fix relative paths for exclude-files.


157-164: Improved unused function reporting.

The updated code now accumulates all unused functions and returns them at once, rather than returning after finding the first one. This provides a more comprehensive report to users.

tests/unused_code/test_unused_code.py (2)

16-19: Correctly updated to use relative path.

The test now properly uses a relative path for the exclude-files option, which aligns with the PR objective to fix relative paths for exclude-files.


25-30: Good test for validating wrong path behavior.

This test effectively validates that using an incorrect path format (just the filename instead of the relative path) doesn't exclude the file as expected.

Comment thread apps/unused_code/README.md Outdated
Comment thread tests/unused_code/unused_code_file_for_test.py
Signed-off-by: lugi0 <lgiorgi@redhat.com>
Signed-off-by: lugi0 <lgiorgi@redhat.com>

def process_file(py_file: str, func_ignore_prefix: list[str], file_ignore_list: list[str]) -> str:
if os.path.basename(py_file) in file_ignore_list:
if os.path.relpath(py_file) in file_ignore_list:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may break existing usage. Expectation is, you would be running unused code check from root directory of your repo and if you want to ignore some file, you would be passing their names. Using relative file path can be problematic here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is IMHO incorrect.
basename would return the tail of https://docs.python.org/3/library/os.path.html#os.path.split, which is assumed to be the file name of a given path.
This means that passing foo/xyz.py would return xyz.py, which might not be what the user has intended to exclude, e.g. in a scenario like

project/
├─ bar/
│  ├─ xyz.py
├─ foo/
│  ├─ xyz.py
├─ xyz.py

passing foo/xyz.py when running from the project root would actually exclude all xyz.py files (project/xyz.py, project/bar/xyz.py and project/foo/xyz.py).
relpath returns the relative path from the current directory - assuming you are excluding files based on their paths from the project root as is the current assumption, you would end up excluding the specific file rather than all of them

with open(py_file) as fd:
tree = ast.parse(source=fd.read())

with open(py_file) as fd:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file was already opened in line 116, can you collect the comments there?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, file.read() returns the entire file contents and any subsequent read in the same context would return ''; the file has to be closed and reopened before its contents can be read again

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

save d.read() and re-use

if _line.strip().startswith("#"):
continue

if _line.strip().startswith("assert"):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please elaborate? Are we currently counting such calls?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently the tool would count a function as being used even if its name was only referenced in an assert statement, e.g. see the test cases of this PR and imagine a scenario in which you are doing
assert "check_me" in output.
In this case "check_me" would suddenly stop being in the output, because the name of the function is now found, and even though the function itself is never actually used the tool incorrectly reports otherwise.
You can try running poetry run pytest with this line commented out to see it in action, but with this a function is only reported as used if it is actually called from some other place other than the assert statement(s).
As I wrote in the earlier comment it is an opinionated call, but I think it is fair to assume that the function name being found does not equal the function itself being used.
One could be more fancy with trying to figure out if the function name is encased in a string delimiter to rule out other such instances, but it would require a fair bit of work to get right

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch, I think the proper way t solve it is to use grep -E

git grep -wE '{func.name}(.*)' | grep -v 'def'

this will also replace
if f"def {func.name}" in _line: line

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The grep will not work, as it will still match the function name in any given context (e.g. print("my_function")). You would need to do negative lookbehind to ensure you're not in a string, but as the wise sage once said:

Some people, when confronted with a problem, think “I know, I’ll use regular expressions.” Now they have two problems.

The only way to properly handle it would be to special case it in the ast parse by matching the instance type with ast.Call, but even then I am not 100% sure of its correctness in all cases. Feel free to improve this as you see fit!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
apps/unused_code/README.md (2)

37-40: Clarify Function Skipping Section
The new section clearly introduces how to skip individual functions via the # skip-unused-code inline comment. To provide extra clarity, consider mentioning that the skip comment should be placed immediately before the function definition. This can help eliminate any ambiguity about its proper positioning.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~39-~39: Possible missing comma found.
Context: ...To skip single functions in your target repository you can add an inline comment to the fu...

(AI_HYDRA_LEO_MISSING_COMMA)


41-42: Improve Markdown Heading Formatting
The heading "### Example:" includes a trailing colon, which does not comply with common markdown style guidelines (e.g., MD026 no-trailing-punctuation). It is recommended to remove the colon so that it reads simply "### Example".

🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

41-41: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9c2c334 and 6c04224.

📒 Files selected for processing (2)
  • apps/unused_code/README.md (1 hunks)
  • tests/unused_code/unused_code_file_for_test.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unused_code/unused_code_file_for_test.py
🧰 Additional context used
🪛 LanguageTool
apps/unused_code/README.md

[uncategorized] ~39-~39: Possible missing comma found.
Context: ...To skip single functions in your target repository you can add an inline comment to the fu...

(AI_HYDRA_LEO_MISSING_COMMA)


[uncategorized] ~43-~43: Possible missing comma found.
Context: ...sed-code` ### Example: Given a target file main.py ```python def tmp1(): # skip-u...

(AI_HYDRA_LEO_MISSING_COMMA)

🪛 markdownlint-cli2 (0.17.2)
apps/unused_code/README.md

41-41: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: python-module-install
  • GitHub Check: tox
🔇 Additional comments (2)
apps/unused_code/README.md (2)

43-53: Validate Inline Code Example
The Python example clearly demonstrates the intended usage of the # skip-unused-code comment by annotating the functions tmp1 and tmp2. The inline comment format matches the documented specification. Ensure that any potential issues raised by static analysis (e.g., a false positive about a missing comma) are verified to be non-issues.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~43-~43: Possible missing comma found.
Context: ...sed-code` ### Example: Given a target file main.py ```python def tmp1(): # skip-u...

(AI_HYDRA_LEO_MISSING_COMMA)


54-60: Verify Output Example Consistency
The provided output sample correctly shows debug log entries for both functions being skipped. This output effectively illustrates the new behavior. Please double-check that this log format aligns with the overall tool’s logging conventions.

SKIP_COMMENT = "# skip-unused-code"


def extract_inline_function_comments(source_code: str) -> dict[str, list[str]]:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

found this to be more simple
https://pypi.org/project/ast-comments/

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion! I wasn't familiar with this library, so I appreciate you bringing it up. My PR was more about implementing the logic directly, but if the preferred approach is to use that library instead, feel free to close this and go ahead with that direction.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why?
You can make this PR to use it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lugi0
Please update if you continue with this PR.
I prefer you will :)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@myakove as I said, if you wish to implement this functionality in a different way feel free to close the PR and move ahead with the proposed solution; you can also merge the PR and then update the code as you see fit, if that is preferable. I unfortunately do not have the time to reimplement the logic using a library I am not familiar with in the short term.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@myakove seems interesting. I will explore it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lugi0 sure
@dbasunag please take it from here

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@myakove I will. - if you don't already create a patch and fix it, before I get a chance!

return ""

with open(py_file) as fd:
tree = ast.parse(source=fd.read())
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from ast_comments import parse

Suggested change
tree = ast.parse(source=fd.read())
tree = parse(source=fd.read())

if func.name in comments.keys():
LOGGER.debug(f"Skipping function due to comment: {func.name}")
continue

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if any(getattr(item, "value", None) == "# skip-unused-code" for item in func.body):
            continue

@myakove
Copy link
Copy Markdown
Collaborator

myakove commented Mar 13, 2025

added in #164

@myakove myakove closed this Mar 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants