Add cross-file import tracking functionality - #1
Conversation
Introduce a new plan for cross-file import tracking, transforming the tool to analyze imports across multiple files. Implement a new CLI interface for cross-file mode, along with a detailed architecture overview. Add new data structures and modules for module resolution, import graph construction, and cross-file analysis. Outline implementation phases and edge cases, ensuring comprehensive testing and verification strategies.
There was a problem hiding this comment.
Pull request overview
This PR introduces cross-file import tracking functionality, transforming the tool from single-file analysis to understanding imports across an entire codebase. The implementation adds the ability to detect re-exported imports (where a module imports something and other files import it from that module), identify implicit re-exports (re-exports not declared in __all__), and detect circular import chains.
Changes:
- Implements a complete module resolution system that mirrors Python's import mechanism
- Adds import graph construction to track dependencies between files
- Introduces cross-file analysis that identifies re-exported imports to avoid false positives
- Updates CLI to default to cross-file mode with
--single-fileflag for backward compatibility
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| CROSS_FILE_PLAN.md | Comprehensive design document outlining architecture, data structures, and implementation phases |
| remove_unused_imports/_data.py | Adds ModuleInfo, ImportEdge, and ImplicitReexport data classes; extends ImportInfo with level field |
| remove_unused_imports/_resolution.py | New module implementing Python-like import resolution with support for relative imports and PYTHONPATH |
| remove_unused_imports/_graph.py | New module for building and analyzing import graphs with cycle detection and topological ordering |
| remove_unused_imports/_cross_file.py | Implements cross-file analysis to identify re-exports and aggregate usage across modules |
| remove_unused_imports/_main.py | Updates CLI to support cross-file mode by default with new flags for warnings and backward compatibility |
| remove_unused_imports/_ast_helpers.py | Adds level parameter to ImportExtractor for relative import support |
| remove_unused_imports/init.py | Exports new public APIs for cross-file analysis |
| tests/resolution_test.py | Comprehensive tests for module resolution including relative imports and PYTHONPATH |
| tests/graph_test.py | Tests for import graph construction, cycle detection, and topological ordering |
| tests/cross_file_test.py | Tests for re-export detection, implicit re-exports, and chained re-exports |
| tests/cli_test.py | Tests for CLI with cross-file mode flags and backward compatibility |
| .pre-commit-config.yaml | Increases max line length from 100 to 110 characters |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Refactor the CrossFileAnalyzer to compute a full cascade of unused imports, allowing for accurate identification of imports that become unused when their consumers are removed. Update the logic for re-exported imports and add comprehensive tests to validate the new cascading behavior.
Refactor the CrossFileAnalyzer to track and report files that become unreachable when unused imports are removed. Introduce logic to identify "reexport-only" and "implicit-reexport-only" imports, improving the accuracy of unused import detection. Update the CLI to support warnings for unreachable files and enhance the formatting of results to include this information. Comprehensive tests added to validate the new functionality.
Test across 20 combinations: - ubuntu-latest (x64) - windows-latest (x64) - macos-13 (Intel x64) - macos-14 (Apple Silicon ARM64) Each with Python 3.10, 3.11, 3.12, 3.13, and 3.14. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- macos-13 -> macos-15-large (Intel x64) - macos-14 -> macos-latest (Apple Silicon ARM64) See: actions/runner-images#13046 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
macos-15-large requires a paid plan. Free tier now only has ARM64 macOS runners available. CI matrix is now 15 jobs: - ubuntu-latest (x64) - windows-latest (x64) - macos-latest (ARM64) Each with Python 3.10-3.14. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Two critical bugs fixed: 1. Module resolution for package directories: When analyzing a package directory (e.g., models/), the source root was incorrectly set to the package itself. This caused imports like `from models import X` within models/user.py to fail resolution (looking for models/models/__init__.py instead of models/__init__.py). Fix: When the target directory has __init__.py, use its parent as the source root so imports resolve correctly. 2. Scope filtering for results: When analyzing a package directory, the graph correctly follows imports to sibling packages (needed for re-export detection), but the results should only report on files within the requested scope. Fix: Filter unused imports, implicit reexports, circular imports, and unreachable files to only include files under the target path. Also: - Added is_under_path() utility to _data.py (single definition) - Added 3 tests covering the fixes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Decorators are evaluated before the function/class name is bound, so
`@foo` must resolve before `def foo` creates a binding. Previously,
we bound the name first, causing decorators with the same name as the
decorated function/class to be incorrectly flagged as unused.
Example that was incorrectly flagged:
```python
from sqlalchemy.orm import reconstructor
class Model:
@ReConstructor # This was flagged as unused
def reconstructor(self):
pass
```
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Test all Python versions (3.10-3.14) on Ubuntu, plus Python 3.12 on Windows and macOS for OS-specific coverage. This is sufficient for a pure Python linter without native extensions. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements standard noqa comment handling matching flake8 behavior: - `# noqa` suppresses all warnings on the line - `# noqa: F401` specifically suppresses unused import warnings - Keyword is case-insensitive, but codes are case-sensitive - Works with multi-line imports (per-name noqa on each line) - Works with backslash continuation lines - Works with semicolon-separated imports Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The tool now skips directories like .venv, node_modules, __pycache__, .git, build, dist, etc. when scanning for Python files. This prevents: - RecursionError crashes from test files with deeply nested AST nodes (e.g., astroid's joined_strings.py test data) - Unnecessary analysis of third-party code and build artifacts - Faster analysis on large codebases Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Document noqa comment support (# noqa: F401) - Document directory exclusions (.venv, node_modules, etc.) - Add noqa examples section Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The tests were using Unix-style absolute paths like `/project/src/a.py` which don't work properly on Windows. When `base_path.resolve()` is called, it converts `/project/src` to something like `D:\project\src`, but the test file paths remain as `/project/src/a.py`, causing `is_under_path()` to fail. Fixed by using pytest's `tmp_path` fixture to create real temporary directories that work correctly on all platforms. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added a feature comparison table showing how remove-unused-imports compares to other Python unused import tools (Ruff, Autoflake, Flake8/Pyflakes, Pylint). Key differentiators highlighted: - Cross-file analysis (unique) - Re-export tracking (unique) - Cascade detection (unique) - Circular import warnings (unique) - Unreachable file warnings (unique) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Deep analysis of Pyflakes, Autoflake, Pylint, Ruff, and Unimport source code to create accurate feature comparison. Key findings: - Added Unimport to comparison (has star import suggestions, type comments) - Corrected Pyflakes noqa support (handled by Flake8 wrapper, not Pyflakes) - Added features we're missing: type comments, redundant alias, star import suggestions, redefinition warnings - All other tools are single-file only; cross-file analysis is unique Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Renamed from remove-unused-imports to import-analyzer-py to better reflect the expanded scope of the tool: - Cross-file import analysis - Re-export tracking - Cascade detection - Circular import warnings - Unreachable file detection Changes: - Renamed module directory: remove_unused_imports/ -> import_analyzer/ - Updated package name in pyproject.toml - Updated CLI entry point: remove-unused-imports -> import-analyzer - Updated all internal imports - Updated all test imports - Updated README.md and CLAUDE.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Cross-File Import Tracking
Summary
This document describes the cross-file import tracking feature that was implemented for the unused import detector. The feature transforms the tool from single-file analysis to cross-file import tracking, following imports like Python's runtime and detecting unused imports across an entire project.
What Was Implemented
Core Features
__all__from pkg import submodulewhere submodule isn't inpkg/__init__.pyCLI Interface (Actual)
Architecture
Data Structures
_data.py_cross_file.pyKey Algorithms
Cascade Detection
The cascade algorithm iteratively finds unused imports until stable:
Unreachable File Detection
Two concepts are tracked separately:
This distinction handles patterns where
import pkg+pkg.submodule.xkeeps submodules accessible at runtime even iffrom pkg import submoduleis unused.Submodule Traversal
When processing
from pkg import name:pkgtopkg/__init__.pynameis already inpkg/__init__.py(imports or definitions)pkg.nameas a submoduleFiles Created/Modified
_data.pyModuleInfo,ImportEdge,ImplicitReexport,levelfield_resolution.py_graph.py_cross_file.py_format.py_ast_helpers.pylevelto ImportExtractor, addedcollect_dunder_all_names_main.py_detection.pyignore_allparameter for reexport-only detectiontests/cross_file_test.pyTest Coverage
Cross-file tests (
tests/cross_file_test.py)test_reexported_import_not_unused,test_chain_of_reexports,test_partial_reexporttest_implicit_reexport_detectedtest_external_usage_aggregatedtest_explicit_reexport_not_flaggedtest_circular_import_detected,test_no_circular_when_nonetest_defined_name_not_reexporttest_cascade_unused_when_consumer_removedtest_cascade_partial_chaintest_cascade_multiple_consumerstest_cascade_reexport_only_via_dunder_alltest_cascade_reexport_only_keeps_usedtest_cascade_file_unreachabletest_cascade_file_still_reachable_via_other_pathtest_submodule_not_in_init_is_traversedtest_submodule_not_unreachable_when_parent_reachabletest_truly_unreachable_file_is_reportedtest_cascade_works_for_potentially_unreachable_but_not_reportedtest_cascade_detects_more_unused_via_potentially_unreachableTotal: 333 tests passing
Example Output
Edge Cases Handled
--warn-circular__init__.pyre-exports: Tracked viaexportsfield (names in__all__)from pkg import submodwheresubmodisn't in__init__.pyPath.samefile()for comparisons (macOS)import pkg+pkg.submod.xkeeps submodules accessibleKnown Limitations
importlib.import_module()cannot be analyzed staticallyfrom x import *uses__all__if available, otherwise skippedifblocks are always consideredpkg.submodaccess without explicit import is not trackedPerformance
Tested on a real-world web application (~100 Python files):
Future Improvements
pkg.submodusage patterns