lapsed 3.2.1__tar.gz → 3.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lapsed-3.2.1 → lapsed-3.2.2}/Cargo.lock +1 -1
- {lapsed-3.2.1 → lapsed-3.2.2}/Cargo.toml +1 -1
- {lapsed-3.2.1 → lapsed-3.2.2}/PKG-INFO +1 -1
- {lapsed-3.2.1 → lapsed-3.2.2}/pyproject.toml +1 -1
- {lapsed-3.2.1 → lapsed-3.2.2}/src/visitor/dead_code_visitor.rs +318 -43
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/fix_special_cases.rs +60 -5
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/ignore_flags_cli.rs +1 -1
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/noqa_comments.rs +12 -0
- lapsed-3.2.2/tests/parity/known_differences.txt +27 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/pytest_and_typing.rs +54 -0
- lapsed-3.2.1/tests/parity/known_differences.txt +0 -36
- {lapsed-3.2.1 → lapsed-3.2.2}/.github/workflows/ci.yml +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/.github/workflows/wheels.yml +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/.gitignore +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/.pre-commit-hooks.yaml +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/AGENTS.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/CLAUDE.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/LICENSE +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/README.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/docs/_static/AGPLv3-license.svg +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/docs/_static/deadcode-logo-readme.png +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/enhancement_proposals/dep_0.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/enhancement_proposals/dep_1.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/enhancement_proposals/dep_2.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/enhancement_proposals/dep_3.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/examples/profile_stages.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/.github/dependabot.yml +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/.github/workflows/check-deadcode-on-python310.yml +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/.github/workflows/test.yml +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/.github/workflows/vulnerability-audit.yml +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/.gitignore +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/.pre-commit-config.yaml +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/.pre-commit-hooks.yaml +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/.readthedocs.yaml +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/LICENSE +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/Makefile +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/README.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/codecov.yml +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/__init__.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/actions/__init__.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/actions/find_python_filenames.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/actions/find_unused_names.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/actions/fix_or_show_unused_code.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/actions/get_unused_import_parts.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/actions/get_unused_names_error_message.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/actions/merge_overlaping_file_parts.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/actions/parse_abstract_syntax_tree.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/actions/parse_arguments.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/actions/remove_file_parts_from_content.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/cli.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/constants.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/data_types.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/utils/__init__.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/utils/add_colors_to_diff.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/utils/fix_indent.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/utils/flatten_lists.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/utils/nested_scopes.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/utils/normalize_path.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/utils/print_ast.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/utils/print_safely.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/visitor/__init__.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/visitor/code_item.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/visitor/dead_code_visitor.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/visitor/ignore.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/visitor/lines.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/visitor/module_exports.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/visitor/noqa.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/visitor/usage_graph.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/visitor/utils.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/_static/AGPLv3-license.svg +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/_static/custom.css +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/_static/deadcode-logo-readme.png +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/comparison.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/conf.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/contributing.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/dep.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/dep_1.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/dep_2.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/dep_3.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/dep_4.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/dep_5.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/dep_6.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/dep_7.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/django.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/fastapi.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/features.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/index.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/installation.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/known_limitations.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/open_source_experiment.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/performance.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/pre_commit.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/release_notes.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/requirements.txt +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/rules.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/rust.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/security.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/usage.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/enhancement_proposals/dep_0.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/enhancement_proposals/dep_1.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/enhancement_proposals/dep_2.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/enhancement_proposals/dep_3.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/enhancement_proposals/dep_4.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/enhancement_proposals/dep_5.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/enhancement_proposals/dep_6.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/enhancement_proposals/dep_7.md +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/pyproject.toml +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/requirements-dev.txt +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/requirements.txt +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/actions/test_find_python_filenames.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/actions/test_get_unused_import_parts.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/actions/test_merge_overlaping_file_parts.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/actions/test_remove_file_parts_from_content.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli/test_cli_exit_status.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli/test_cli_module_entry_point.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli/test_cli_output_encoding.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli/test_deadcode.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli/test_output.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli/test_verbose_output_encoding.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/__init__.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_argument_routes.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_count.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_dry.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_exclude.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_fix.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_bodies_if_decorated_with.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_bodies_if_inherits_from.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_bodies_of.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_definitions.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_definitions_if_decorated_with.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_definitions_if_inherits_from.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_names.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_names_if_decorated_with.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_names_if_inherits_from.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_names_in_files.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_option_combinations.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_rules.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_no_color.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_only.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_parsing.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_pyproject_boolean_options.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_pyproject_list_options.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_quiet.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_verbose.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_version.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/code_item/test_code_item.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/configuration/test_decorator_pyproject_configuration.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/conftest.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/documented_examples/test_django.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/documented_examples/test_documented_features.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/documented_examples/test_documented_options.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/documented_examples/test_documented_rules.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/documented_examples/test_fastapi.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/documented_examples/test_fastapi_dependency_injection.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/documented_examples/test_fastapi_documentation.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/documented_examples/test_pre_commit_hook_configuration.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/documented_examples/test_readme_coverage_badge.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/classes.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/admin.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/api.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/apps.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/forms.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/import_forms.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/management/commands/import_data.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/managers.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/middleware.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/migrations/0001_initial.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/models.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/settings.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/signals.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/templatetags/blog_tags.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/tests.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/urls.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/views.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi/dependencies.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi/main.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi/models.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi/routes/users.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi_dependency_injection/dependencies.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi_dependency_injection/main.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi_dependency_injection/models.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi_dependency_injection/security.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi_features/background.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi_features/errors.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi_features/main.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi_features/middleware.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi_features/models.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi_features/routes/items.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi_features/security.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/functions.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/subdir/another_subdir/__init__.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/subdir/one_more_file.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/variables.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/__init__.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/test_assign.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/test_attribute.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/test_cascading_dead_code.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/test_class_def.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/test_commented_out_code.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/test_empty_files.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/test_method_def.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/test_property.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/test_scope.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/test_unreachable_code.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/test_unused_imports.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/nested_scope/test_nested_scope.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/noqa/test_noqa_comments.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/regression/__init__.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/regression/test_cascading_dead_code.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/regression/test_name_shadowing.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/regression/test_python314_attribute_error.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/utils/__init__.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/utils/test_add_colors_to_diff.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/utils/test_fix_indent.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/utils/test_normalize_path.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/utils/test_print_ast.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/utils/test_print_safely.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/__init__.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_accurate_usage_detection.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_commented_out_code.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_decorator_without_id_attribute.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_django_lifecycle_methods.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_ignore.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_ignore_interface_decorators.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_imports.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_line_number_detection.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_middleware_registration.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_name_usage_and_unreachable_detection.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_qualified_name_resolution.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_unittest_lifecycle_methods.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_utils.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_variable_parsing_from_ast_dump.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_wildcard_imports.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/scripts/check.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/scripts/parity.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/actions/find_python_filenames.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/actions/fix_or_show_unused_code.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/actions/get_unused_import_parts.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/actions/get_unused_names_error_message.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/actions/merge_overlapping_file_parts.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/actions/parse_abstract_syntax_tree.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/actions/parse_arguments.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/actions/parse_tach_config.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/actions/remove_file_parts_from_content.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/cli.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/constants.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/data_types.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/lib.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/main.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/utils/add_colors_to_diff.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/utils/fix_indent.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/utils/fnmatch.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/utils/line_index.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/utils/nested_scopes.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/utils/path_utils.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/visitor/code_item.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/visitor/ignore.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/visitor/noqa.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/src/visitor/utils.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/aspirational_unimplemented.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/attrs_field_hooks.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/common/mod.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/deadcode_integration.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/exit_code.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/files/classes.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/files/functions.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/files/subdir/another_subdir/__init__.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/files/subdir/one_more_file.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/files/variables.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/fix_cli_option.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/imports_and_output.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/line_number_detection.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/modern_python_syntax.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/absimport/main.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/absimport/pkg/__init__.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/absimport/pkg/a.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/abstract/m.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/cascade/m.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/dc06/m.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/dc09/m.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/dc09b/m.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/dc12/m.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/dc13/m.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/django/m.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/django_mw/proj/middleware.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/django_mw/proj/settings.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/dotted_base/m.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/fastapi/m.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/fix_edge_cases/m.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/imports_fix/m.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/multiline_assign/m.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/noqa_decorated/m.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/pyproject_options/m.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/pyproject_options/pyproject.toml +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/quoted/m.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/samename/a.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/samename/b.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/samename/c.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/shadowing/m.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/typecomment/m.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/unicode/m.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/wildcard/m.py +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/scope_tracking.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/tach_config_cli.rs +0 -0
- {lapsed-3.2.1 → lapsed-3.2.2}/tests/version_consistency.rs +0 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# The command users type and the crate name used in `use deadcode::...` are
|
|
4
4
|
# set by [[bin]] and [lib] below and deliberately stay `deadcode`.
|
|
5
5
|
name = "lapsed"
|
|
6
|
-
version = "3.2.
|
|
6
|
+
version = "3.2.2"
|
|
7
7
|
edition = "2021"
|
|
8
8
|
rust-version = "1.95"
|
|
9
9
|
description = "Find and remove dead code in Python codebases."
|
|
@@ -45,6 +45,9 @@ enum DefinedKind {
|
|
|
45
45
|
Method,
|
|
46
46
|
Property,
|
|
47
47
|
Variable,
|
|
48
|
+
/// A variable assigned directly in a class body: upstream's DC06
|
|
49
|
+
/// "unused name" rather than DC01.
|
|
50
|
+
ClassBodyName,
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
impl DefinedKind {
|
|
@@ -57,6 +60,7 @@ impl DefinedKind {
|
|
|
57
60
|
DefinedKind::Method => UnusedCodeType::Method,
|
|
58
61
|
DefinedKind::Property => UnusedCodeType::Property,
|
|
59
62
|
DefinedKind::Variable => UnusedCodeType::Variable,
|
|
63
|
+
DefinedKind::ClassBodyName => UnusedCodeType::Name,
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
}
|
|
@@ -109,6 +113,52 @@ pub struct DeadCodeVisitor<'a> {
|
|
|
109
113
|
assignment_removal_range: Option<TextRange>,
|
|
110
114
|
}
|
|
111
115
|
|
|
116
|
+
/// Names loaded by the expressions inside string literals nested in
|
|
117
|
+
/// `annotation`, following strings inside those strings up to `depth` levels.
|
|
118
|
+
/// Mirrors upstream's `collect_names_from_annotation_strings`.
|
|
119
|
+
fn annotation_string_names(annotation: &Expr, depth: u32) -> HashSet<String> {
|
|
120
|
+
use ruff_python_ast::visitor::{walk_expr, Visitor};
|
|
121
|
+
|
|
122
|
+
#[derive(Default)]
|
|
123
|
+
struct Collector {
|
|
124
|
+
strings: Vec<String>,
|
|
125
|
+
names: HashSet<String>,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
impl<'a> Visitor<'a> for Collector {
|
|
129
|
+
fn visit_expr(&mut self, expr: &'a Expr) {
|
|
130
|
+
match expr {
|
|
131
|
+
Expr::StringLiteral(literal) => {
|
|
132
|
+
self.strings.push(literal.value.to_str().to_string())
|
|
133
|
+
}
|
|
134
|
+
Expr::Name(name) if matches!(name.ctx, ExprContext::Load | ExprContext::Del) => {
|
|
135
|
+
self.names.insert(name.id.to_string());
|
|
136
|
+
}
|
|
137
|
+
_ => {}
|
|
138
|
+
}
|
|
139
|
+
walk_expr(self, expr);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let mut outer = Collector::default();
|
|
144
|
+
outer.visit_expr(annotation);
|
|
145
|
+
let mut names = HashSet::new();
|
|
146
|
+
if depth == 0 {
|
|
147
|
+
return names;
|
|
148
|
+
}
|
|
149
|
+
for string in outer.strings {
|
|
150
|
+
let Ok(parsed) = ruff_python_parser::parse_expression(&string) else {
|
|
151
|
+
continue;
|
|
152
|
+
};
|
|
153
|
+
let expression = &parsed.syntax().body;
|
|
154
|
+
let mut inner = Collector::default();
|
|
155
|
+
inner.visit_expr(expression);
|
|
156
|
+
names.extend(inner.names);
|
|
157
|
+
names.extend(annotation_string_names(expression, depth - 1));
|
|
158
|
+
}
|
|
159
|
+
names
|
|
160
|
+
}
|
|
161
|
+
|
|
112
162
|
/// Extracts a `str` constant value, mirroring the several
|
|
113
163
|
/// `isinstance(x, ast.Str)` checks in the Python original.
|
|
114
164
|
///
|
|
@@ -358,9 +408,25 @@ impl<'a> DeadCodeVisitor<'a> {
|
|
|
358
408
|
/// Record a DC09 finding. The caller chooses the precise source span to
|
|
359
409
|
/// remove: a false condition removes the full statement, while an always
|
|
360
410
|
/// true condition removes just its unreachable `else` suite.
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
411
|
+
/// Records a DC09 finding.
|
|
412
|
+
///
|
|
413
|
+
/// Upstream reports it at `position` (the last statement of the dead
|
|
414
|
+
/// body), honours `noqa` on the line of `first`, `position` and any
|
|
415
|
+
/// `extra_noqa_line` (the `else:` keyword), and removes `removal` -- or
|
|
416
|
+
/// nothing, where no removal keeps the code valid with its behaviour
|
|
417
|
+
/// unchanged.
|
|
418
|
+
fn add_unreachable_body(
|
|
419
|
+
&mut self,
|
|
420
|
+
name: &str,
|
|
421
|
+
first: TextSize,
|
|
422
|
+
position: TextSize,
|
|
423
|
+
removal: Option<TextRange>,
|
|
424
|
+
message: &str,
|
|
425
|
+
extra_noqa_line: Option<u32>,
|
|
426
|
+
) {
|
|
427
|
+
let (first_line, _) = self.line_index.line_col(first);
|
|
428
|
+
let (line, column) = self.line_index.line_col(position);
|
|
429
|
+
let noqa_lines = [Some(first_line as u32), Some(line as u32), extra_noqa_line];
|
|
364
430
|
if self.should_ignore_new_definitions
|
|
365
431
|
|| fnmatch::match_any(name, &self.args.ignore_names, true)
|
|
366
432
|
|| fnmatch::match_any(
|
|
@@ -368,20 +434,31 @@ impl<'a> DeadCodeVisitor<'a> {
|
|
|
368
434
|
&self.args.ignore_names_in_files,
|
|
369
435
|
true,
|
|
370
436
|
)
|
|
371
|
-
||
|
|
437
|
+
|| noqa_lines
|
|
438
|
+
.into_iter()
|
|
439
|
+
.flatten()
|
|
440
|
+
.any(|noqa_line| noqa::ignore_line(&self.noqa_lines, noqa_line, "DC09"))
|
|
372
441
|
{
|
|
373
442
|
return;
|
|
374
443
|
}
|
|
444
|
+
let code_parts = removal
|
|
445
|
+
.map(|range| {
|
|
446
|
+
let (start_line, start_column) = self.line_index.line_col(range.start());
|
|
447
|
+
let (end_line, end_column) = self.line_index.line_col(range.end());
|
|
448
|
+
Part::new(
|
|
449
|
+
start_line as u32,
|
|
450
|
+
end_line as u32,
|
|
451
|
+
start_column as u32,
|
|
452
|
+
end_column as u32,
|
|
453
|
+
)
|
|
454
|
+
})
|
|
455
|
+
.into_iter()
|
|
456
|
+
.collect();
|
|
375
457
|
self.unreachable_body.push(CodeItem::new(
|
|
376
458
|
name.to_string(),
|
|
377
459
|
UnusedCodeType::UnreachableBody,
|
|
378
460
|
self.filename.clone(),
|
|
379
|
-
|
|
380
|
-
line as u32,
|
|
381
|
-
end_line as u32,
|
|
382
|
-
column as u32,
|
|
383
|
-
end_column as u32,
|
|
384
|
-
)],
|
|
461
|
+
code_parts,
|
|
385
462
|
(!self.scope_parts.is_empty()).then(|| self.scope_parts.join(".")),
|
|
386
463
|
None,
|
|
387
464
|
Some(line as u32),
|
|
@@ -390,13 +467,145 @@ impl<'a> DeadCodeVisitor<'a> {
|
|
|
390
467
|
));
|
|
391
468
|
}
|
|
392
469
|
|
|
470
|
+
/// DC09 for an `if` (or, with `clauses` holding what follows it, an
|
|
471
|
+
/// `elif`). Upstream sees an `elif` as an `If` nested in `orelse`, so each
|
|
472
|
+
/// `elif` in a chain is checked as its own statement starting at its
|
|
473
|
+
/// keyword (`start`).
|
|
474
|
+
fn check_if_condition(
|
|
475
|
+
&mut self,
|
|
476
|
+
start: TextSize,
|
|
477
|
+
test: &Expr,
|
|
478
|
+
body: &[Stmt],
|
|
479
|
+
clauses: &[ruff_python_ast::ElifElseClause],
|
|
480
|
+
end: TextSize,
|
|
481
|
+
) {
|
|
482
|
+
let (Some(first), Some(last)) = (body.first(), body.last()) else {
|
|
483
|
+
return;
|
|
484
|
+
};
|
|
485
|
+
match Self::conditional_outcome(test) {
|
|
486
|
+
Some(false) => {
|
|
487
|
+
// Upstream removes the whole statement, `elif`/`else`
|
|
488
|
+
// included, deleting reachable code. With clauses following,
|
|
489
|
+
// empty the body instead -- the remover puts `pass` there.
|
|
490
|
+
let removal = if clauses.is_empty() {
|
|
491
|
+
TextRange::new(start, end)
|
|
492
|
+
} else {
|
|
493
|
+
TextRange::new(first.start(), last.end())
|
|
494
|
+
};
|
|
495
|
+
self.add_unreachable_body(
|
|
496
|
+
"if",
|
|
497
|
+
start,
|
|
498
|
+
last.start(),
|
|
499
|
+
Some(removal),
|
|
500
|
+
"Unsatisfiable `if` condition",
|
|
501
|
+
None,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
Some(true) => match clauses.first() {
|
|
505
|
+
// `elif`: everything from its keyword on is unreachable.
|
|
506
|
+
Some(clause) if clause.test.is_some() => self.add_unreachable_body(
|
|
507
|
+
"else",
|
|
508
|
+
clause.start(),
|
|
509
|
+
clause.start(),
|
|
510
|
+
Some(TextRange::new(clause.start(), end)),
|
|
511
|
+
"Unreachable `else` block",
|
|
512
|
+
None,
|
|
513
|
+
),
|
|
514
|
+
Some(clause) => self.add_unreachable_else(clause.start(), &clause.body),
|
|
515
|
+
// Upstream removes the whole statement, body included, which
|
|
516
|
+
// deletes code that always runs. Report it, but leave the
|
|
517
|
+
// code alone: dedenting the body is not a removal.
|
|
518
|
+
None => self.add_unreachable_body(
|
|
519
|
+
"if",
|
|
520
|
+
start,
|
|
521
|
+
start,
|
|
522
|
+
None,
|
|
523
|
+
"Redundant `if` condition",
|
|
524
|
+
None,
|
|
525
|
+
),
|
|
526
|
+
},
|
|
527
|
+
None => {}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/// An `else:` block that can never run, reported on its last statement.
|
|
532
|
+
fn add_unreachable_else(&mut self, else_keyword: TextSize, body: &[Stmt]) {
|
|
533
|
+
let (Some(first), Some(last)) = (body.first(), body.last()) else {
|
|
534
|
+
return;
|
|
535
|
+
};
|
|
536
|
+
let (else_line, _) = self.line_index.line_col(else_keyword);
|
|
537
|
+
self.add_unreachable_body(
|
|
538
|
+
"else",
|
|
539
|
+
first.start(),
|
|
540
|
+
last.start(),
|
|
541
|
+
Some(TextRange::new(first.start(), last.end())),
|
|
542
|
+
"Unreachable `else` block",
|
|
543
|
+
Some(else_line as u32),
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/// Where the `else` keyword before `body` starts: the last `else` between
|
|
548
|
+
/// `after` and the body's first statement.
|
|
549
|
+
fn else_keyword_before(&self, after: TextSize, body: &[Stmt]) -> TextSize {
|
|
550
|
+
let Some(first) = body.first() else {
|
|
551
|
+
return after;
|
|
552
|
+
};
|
|
553
|
+
let between = &self.source[after.to_usize()..first.start().to_usize()];
|
|
554
|
+
between
|
|
555
|
+
.rfind("else")
|
|
556
|
+
.map_or(first.start(), |offset| after + TextSize::new(offset as u32))
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/// The truth value of a constant `ast.literal_eval` would accept, or
|
|
560
|
+
/// `None` for anything it would reject.
|
|
561
|
+
fn literal_truth_value(expression: &Expr) -> Option<bool> {
|
|
562
|
+
use ruff_python_ast::{Number, UnaryOp};
|
|
563
|
+
match expression {
|
|
564
|
+
Expr::BooleanLiteral(value) => Some(value.value),
|
|
565
|
+
Expr::NoneLiteral(_) => Some(false),
|
|
566
|
+
Expr::NumberLiteral(value) => Some(match &value.value {
|
|
567
|
+
Number::Int(int) => *int != ruff_python_ast::Int::ZERO,
|
|
568
|
+
Number::Float(float) => *float != 0.0,
|
|
569
|
+
Number::Complex { real, imag } => *real != 0.0 || *imag != 0.0,
|
|
570
|
+
}),
|
|
571
|
+
Expr::StringLiteral(value) => Some(!value.value.to_str().is_empty()),
|
|
572
|
+
Expr::BytesLiteral(value) => Some(value.value.bytes().next().is_some()),
|
|
573
|
+
Expr::EllipsisLiteral(_) => Some(true),
|
|
574
|
+
Expr::UnaryOp(value)
|
|
575
|
+
if matches!(value.op, UnaryOp::USub | UnaryOp::UAdd)
|
|
576
|
+
&& matches!(value.operand.as_ref(), Expr::NumberLiteral(_)) =>
|
|
577
|
+
{
|
|
578
|
+
Self::literal_truth_value(&value.operand)
|
|
579
|
+
}
|
|
580
|
+
Expr::Tuple(value) => Self::container_truth_value(&value.elts),
|
|
581
|
+
Expr::List(value) => Self::container_truth_value(&value.elts),
|
|
582
|
+
Expr::Set(value) => Self::container_truth_value(&value.elts),
|
|
583
|
+
Expr::Dict(value) => value
|
|
584
|
+
.items
|
|
585
|
+
.iter()
|
|
586
|
+
.all(|item| {
|
|
587
|
+
item.key
|
|
588
|
+
.as_ref()
|
|
589
|
+
.is_some_and(|key| Self::literal_truth_value(key).is_some())
|
|
590
|
+
&& Self::literal_truth_value(&item.value).is_some()
|
|
591
|
+
})
|
|
592
|
+
.then_some(!value.items.is_empty()),
|
|
593
|
+
_ => None,
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
fn container_truth_value(elements: &[Expr]) -> Option<bool> {
|
|
598
|
+
elements
|
|
599
|
+
.iter()
|
|
600
|
+
.all(|element| Self::literal_truth_value(element).is_some())
|
|
601
|
+
.then_some(!elements.is_empty())
|
|
602
|
+
}
|
|
603
|
+
|
|
393
604
|
/// Mirror upstream's safe, conservative condition evaluator. Unknown
|
|
394
605
|
/// expressions are substituted with both truth values, so only conditions
|
|
395
606
|
/// that are provably true or false produce a finding.
|
|
396
607
|
fn condition_truth_value(expression: &Expr, unknown_value: bool) -> bool {
|
|
397
608
|
match expression {
|
|
398
|
-
Expr::BooleanLiteral(value) => value.value,
|
|
399
|
-
Expr::NoneLiteral(_) => false,
|
|
400
609
|
Expr::BoolOp(value) => match value.op {
|
|
401
610
|
BoolOp::And => value
|
|
402
611
|
.values
|
|
@@ -410,7 +619,7 @@ impl<'a> DeadCodeVisitor<'a> {
|
|
|
410
619
|
Expr::UnaryOp(value) if value.op == ruff_python_ast::UnaryOp::Not => {
|
|
411
620
|
!Self::condition_truth_value(&value.operand, !unknown_value)
|
|
412
621
|
}
|
|
413
|
-
_ => unknown_value,
|
|
622
|
+
_ => Self::literal_truth_value(expression).unwrap_or(unknown_value),
|
|
414
623
|
}
|
|
415
624
|
}
|
|
416
625
|
|
|
@@ -740,7 +949,7 @@ impl<'a> DeadCodeVisitor<'a> {
|
|
|
740
949
|
DefinedKind::Import => self.defined_imports.push(code_item),
|
|
741
950
|
DefinedKind::Method => self.defined_methods.push(code_item),
|
|
742
951
|
DefinedKind::Property => self.defined_props.push(code_item),
|
|
743
|
-
DefinedKind::Variable => self.defined_vars.push(code_item),
|
|
952
|
+
DefinedKind::Variable | DefinedKind::ClassBodyName => self.defined_vars.push(code_item),
|
|
744
953
|
}
|
|
745
954
|
}
|
|
746
955
|
|
|
@@ -751,7 +960,12 @@ impl<'a> DeadCodeVisitor<'a> {
|
|
|
751
960
|
let ignored = ignore::ignore_variable(name);
|
|
752
961
|
let removal_range = self.assignment_removal_range.take();
|
|
753
962
|
let recorded_before = self.defined_vars.len();
|
|
754
|
-
|
|
963
|
+
let kind = if self.is_directly_in_class_body() {
|
|
964
|
+
DefinedKind::ClassBodyName
|
|
965
|
+
} else {
|
|
966
|
+
DefinedKind::Variable
|
|
967
|
+
};
|
|
968
|
+
self.push_definition(kind, name, start, end, &[], ignored, None);
|
|
755
969
|
if let Some(range) = removal_range.filter(|_| self.defined_vars.len() > recorded_before) {
|
|
756
970
|
self.widen_last_variable_to(range);
|
|
757
971
|
}
|
|
@@ -1195,7 +1409,7 @@ impl<'a> DeadCodeVisitor<'a> {
|
|
|
1195
1409
|
}
|
|
1196
1410
|
self.walk_parameters(&f.parameters);
|
|
1197
1411
|
if let Some(returns) = &f.returns {
|
|
1198
|
-
self.
|
|
1412
|
+
self.walk_annotation(returns);
|
|
1199
1413
|
}
|
|
1200
1414
|
self.walk_stmt_list(&f.body);
|
|
1201
1415
|
if should_turn_off {
|
|
@@ -1223,6 +1437,28 @@ impl<'a> DeadCodeVisitor<'a> {
|
|
|
1223
1437
|
}
|
|
1224
1438
|
}
|
|
1225
1439
|
}
|
|
1440
|
+
// Django instantiates each class named in a module-level
|
|
1441
|
+
// `MIDDLEWARE` list of dotted paths, so the final component
|
|
1442
|
+
// is used.
|
|
1443
|
+
if let ([Expr::Name(target)], Expr::List(_) | Expr::Tuple(_), true) = (
|
|
1444
|
+
a.targets.as_slice(),
|
|
1445
|
+
a.value.as_ref(),
|
|
1446
|
+
self.scope_parts.len() == 1,
|
|
1447
|
+
) {
|
|
1448
|
+
if target.id.as_str() == "MIDDLEWARE" {
|
|
1449
|
+
let elts: &[Expr] = match a.value.as_ref() {
|
|
1450
|
+
Expr::List(l) => &l.elts,
|
|
1451
|
+
Expr::Tuple(t) => &t.elts,
|
|
1452
|
+
_ => &[],
|
|
1453
|
+
};
|
|
1454
|
+
for path in elts.iter().filter_map(as_str_constant) {
|
|
1455
|
+
let class_name = path.rsplit('.').next().unwrap_or(path);
|
|
1456
|
+
if !class_name.is_empty() {
|
|
1457
|
+
self.add_used_name(class_name);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1226
1462
|
for target in &a.targets {
|
|
1227
1463
|
self.walk_assignment_target(target, a.range, a.targets.len() == 1, false);
|
|
1228
1464
|
}
|
|
@@ -1230,7 +1466,7 @@ impl<'a> DeadCodeVisitor<'a> {
|
|
|
1230
1466
|
}
|
|
1231
1467
|
Stmt::AnnAssign(a) => {
|
|
1232
1468
|
self.walk_assignment_target(&a.target, a.range, true, true);
|
|
1233
|
-
self.
|
|
1469
|
+
self.walk_annotation(&a.annotation);
|
|
1234
1470
|
if let Some(v) = &a.value {
|
|
1235
1471
|
self.walk_expr(v);
|
|
1236
1472
|
}
|
|
@@ -1258,17 +1494,28 @@ impl<'a> DeadCodeVisitor<'a> {
|
|
|
1258
1494
|
}
|
|
1259
1495
|
Stmt::While(s) => {
|
|
1260
1496
|
match Self::conditional_outcome(&s.test) {
|
|
1261
|
-
Some(false) =>
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1497
|
+
Some(false) => {
|
|
1498
|
+
if let (Some(first), Some(last)) = (s.body.first(), s.body.last()) {
|
|
1499
|
+
// As for `if`: an `else:` still runs, so keep it.
|
|
1500
|
+
let removal = if s.orelse.is_empty() {
|
|
1501
|
+
s.range
|
|
1502
|
+
} else {
|
|
1503
|
+
TextRange::new(first.start(), last.end())
|
|
1504
|
+
};
|
|
1505
|
+
self.add_unreachable_body(
|
|
1506
|
+
"while",
|
|
1507
|
+
s.start(),
|
|
1508
|
+
last.start(),
|
|
1509
|
+
Some(removal),
|
|
1510
|
+
"Unsatisfiable `while` condition",
|
|
1511
|
+
None,
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1266
1515
|
Some(true) if !s.orelse.is_empty() => {
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
"Unreachable `else` block",
|
|
1271
|
-
);
|
|
1516
|
+
let after_body = s.body.last().map_or(s.start(), Ranged::end);
|
|
1517
|
+
let else_keyword = self.else_keyword_before(after_body, &s.orelse);
|
|
1518
|
+
self.add_unreachable_else(else_keyword, &s.orelse);
|
|
1272
1519
|
}
|
|
1273
1520
|
_ => {}
|
|
1274
1521
|
}
|
|
@@ -1277,20 +1524,17 @@ impl<'a> DeadCodeVisitor<'a> {
|
|
|
1277
1524
|
self.walk_stmt_list(&s.orelse);
|
|
1278
1525
|
}
|
|
1279
1526
|
Stmt::If(s) => {
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
);
|
|
1291
|
-
}
|
|
1527
|
+
self.check_if_condition(s.start(), &s.test, &s.body, &s.elif_else_clauses, s.end());
|
|
1528
|
+
for (index, clause) in s.elif_else_clauses.iter().enumerate() {
|
|
1529
|
+
if let Some(test) = &clause.test {
|
|
1530
|
+
self.check_if_condition(
|
|
1531
|
+
clause.start(),
|
|
1532
|
+
test,
|
|
1533
|
+
&clause.body,
|
|
1534
|
+
&s.elif_else_clauses[index + 1..],
|
|
1535
|
+
s.end(),
|
|
1536
|
+
);
|
|
1292
1537
|
}
|
|
1293
|
-
_ => {}
|
|
1294
1538
|
}
|
|
1295
1539
|
self.walk_expr(&s.test);
|
|
1296
1540
|
self.walk_stmt_list(&s.body);
|
|
@@ -1379,7 +1623,7 @@ impl<'a> DeadCodeVisitor<'a> {
|
|
|
1379
1623
|
.chain(parameters.kwonlyargs.iter())
|
|
1380
1624
|
{
|
|
1381
1625
|
if let Some(annotation) = &a.parameter.annotation {
|
|
1382
|
-
self.
|
|
1626
|
+
self.walk_annotation(annotation);
|
|
1383
1627
|
}
|
|
1384
1628
|
if let Some(default) = &a.default {
|
|
1385
1629
|
self.walk_expr(default);
|
|
@@ -1387,16 +1631,25 @@ impl<'a> DeadCodeVisitor<'a> {
|
|
|
1387
1631
|
}
|
|
1388
1632
|
if let Some(vararg) = ¶meters.vararg {
|
|
1389
1633
|
if let Some(annotation) = &vararg.annotation {
|
|
1390
|
-
self.
|
|
1634
|
+
self.walk_annotation(annotation);
|
|
1391
1635
|
}
|
|
1392
1636
|
}
|
|
1393
1637
|
if let Some(kwarg) = ¶meters.kwarg {
|
|
1394
1638
|
if let Some(annotation) = &kwarg.annotation {
|
|
1395
|
-
self.
|
|
1639
|
+
self.walk_annotation(annotation);
|
|
1396
1640
|
}
|
|
1397
1641
|
}
|
|
1398
1642
|
}
|
|
1399
1643
|
|
|
1644
|
+
/// Walks a type annotation, also counting names inside quoted (forward
|
|
1645
|
+
/// reference) annotations such as `"OrderedDict[str, int]"` as used.
|
|
1646
|
+
fn walk_annotation(&mut self, annotation: &Expr) {
|
|
1647
|
+
self.walk_expr(annotation);
|
|
1648
|
+
for name in annotation_string_names(annotation, 10) {
|
|
1649
|
+
self.add_used_name(&name);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1400
1653
|
fn walk_expr(&mut self, expr: &Expr) {
|
|
1401
1654
|
match expr {
|
|
1402
1655
|
Expr::Name(n) => {
|
|
@@ -1440,6 +1693,28 @@ impl<'a> DeadCodeVisitor<'a> {
|
|
|
1440
1693
|
self.walk_expr(&l.body);
|
|
1441
1694
|
}
|
|
1442
1695
|
Expr::If(e) => {
|
|
1696
|
+
// Reported, never removed: cutting an operand out of an
|
|
1697
|
+
// expression cannot leave valid code (upstream's removal
|
|
1698
|
+
// leaves `x = ` or `a if True else`).
|
|
1699
|
+
match Self::conditional_outcome(&e.test) {
|
|
1700
|
+
Some(false) => self.add_unreachable_body(
|
|
1701
|
+
"ternary",
|
|
1702
|
+
e.start(),
|
|
1703
|
+
e.body.start(),
|
|
1704
|
+
None,
|
|
1705
|
+
"Unsatisfiable `ternary` condition",
|
|
1706
|
+
None,
|
|
1707
|
+
),
|
|
1708
|
+
Some(true) => self.add_unreachable_body(
|
|
1709
|
+
"ternary",
|
|
1710
|
+
e.orelse.start(),
|
|
1711
|
+
e.orelse.start(),
|
|
1712
|
+
None,
|
|
1713
|
+
"Unreachable `else` expression",
|
|
1714
|
+
None,
|
|
1715
|
+
),
|
|
1716
|
+
None => {}
|
|
1717
|
+
}
|
|
1443
1718
|
self.walk_expr(&e.test);
|
|
1444
1719
|
self.walk_expr(&e.body);
|
|
1445
1720
|
self.walk_expr(&e.orelse);
|
|
@@ -50,13 +50,65 @@ mod unreachable_literal_condition {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
#[test]
|
|
53
|
-
fn
|
|
53
|
+
fn false_if_without_else_removes_the_full_statement() {
|
|
54
|
+
let p = Project::new();
|
|
55
|
+
p.write("foo.py", "if False:\n print(\"a\")\nprint(\"b\")\n");
|
|
56
|
+
let result = p.run(&["foo.py", "--no-color", "--fix"]).unwrap();
|
|
57
|
+
assert!(result.contains("foo.py:2:4: DC09 Unsatisfiable `if` condition"));
|
|
58
|
+
assert_eq!(p.read("foo.py"), "print(\"b\")\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Upstream removes the whole statement here, `else:` included -- deleting
|
|
62
|
+
/// code that always runs.
|
|
63
|
+
#[test]
|
|
64
|
+
fn false_if_keeps_its_reachable_else() {
|
|
54
65
|
let p = Project::new();
|
|
55
66
|
let src = "if False:\n print(\"a\")\nelse:\n print(\"b\")\n";
|
|
56
67
|
p.write("foo.py", src);
|
|
68
|
+
p.run(&["foo.py", "--no-color", "--fix"]).unwrap();
|
|
69
|
+
assert_eq!(
|
|
70
|
+
p.read("foo.py"),
|
|
71
|
+
"if False:\n pass\nelse:\n print(\"b\")\n"
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#[test]
|
|
76
|
+
fn true_if_removes_the_whole_elif_chain() {
|
|
77
|
+
let p = Project::new();
|
|
78
|
+
let src = "import sys\nif True:\n print(\"c\")\nelif sys.argv:\n print(\"d\")\n print(\"e\")\nelse:\n print(\"f\")\n";
|
|
79
|
+
p.write("foo.py", src);
|
|
57
80
|
let result = p.run(&["foo.py", "--no-color", "--fix"]).unwrap();
|
|
58
|
-
assert!(result.contains("DC09
|
|
59
|
-
|
|
81
|
+
assert!(result.contains("foo.py:4:0: DC09 Unreachable `else` block"));
|
|
82
|
+
assert_eq!(p.read("foo.py"), "import sys\nif True:\n print(\"c\")\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#[test]
|
|
86
|
+
fn numeric_string_and_container_constants_are_evaluated() {
|
|
87
|
+
let p = Project::new();
|
|
88
|
+
p.write(
|
|
89
|
+
"foo.py",
|
|
90
|
+
"while 0:\n print(1)\nif \"\":\n print(2)\nif []:\n print(3)\nif (1,):\n print(4)\n",
|
|
91
|
+
);
|
|
92
|
+
let result = p.run(&["foo.py", "--no-color"]).unwrap();
|
|
93
|
+
assert!(result.contains("foo.py:2:4: DC09 Unsatisfiable `while` condition"));
|
|
94
|
+
assert!(result.contains("foo.py:4:4: DC09 Unsatisfiable `if` condition"));
|
|
95
|
+
assert!(result.contains("foo.py:6:4: DC09 Unsatisfiable `if` condition"));
|
|
96
|
+
assert!(result.contains("foo.py:7:0: DC09 Redundant `if` condition"));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Upstream removes a redundant `if True:` body that always runs, and
|
|
100
|
+
/// cuts ternary operands out leaving invalid code.
|
|
101
|
+
#[test]
|
|
102
|
+
fn redundant_if_and_ternaries_are_reported_but_not_removed() {
|
|
103
|
+
let p = Project::new();
|
|
104
|
+
let src =
|
|
105
|
+
"if True:\n print(1)\nx = 1 if False else 2\ny = 1 if True else 2\nprint(x, y)\n";
|
|
106
|
+
p.write("foo.py", src);
|
|
107
|
+
let result = p.run(&["foo.py", "--no-color", "--fix"]).unwrap();
|
|
108
|
+
assert!(result.contains("foo.py:1:0: DC09 Redundant `if` condition"));
|
|
109
|
+
assert!(result.contains("foo.py:3:4: DC09 Unsatisfiable `ternary` condition"));
|
|
110
|
+
assert!(result.contains("foo.py:4:19: DC09 Unreachable `else` expression"));
|
|
111
|
+
assert_eq!(p.read("foo.py"), src);
|
|
60
112
|
}
|
|
61
113
|
|
|
62
114
|
#[test]
|
|
@@ -73,13 +125,16 @@ mod unreachable_literal_condition {
|
|
|
73
125
|
}
|
|
74
126
|
|
|
75
127
|
#[test]
|
|
76
|
-
fn
|
|
128
|
+
fn false_while_keeps_its_reachable_else() {
|
|
77
129
|
let p = Project::new();
|
|
78
130
|
let src = "while False:\n print(\"a\")\nelse:\n print(\"b\")\n";
|
|
79
131
|
p.write("foo.py", src);
|
|
80
132
|
let result = p.run(&["foo.py", "--no-color", "--fix"]).unwrap();
|
|
81
133
|
assert!(result.contains("DC09 Unsatisfiable `while` condition"));
|
|
82
|
-
|
|
134
|
+
assert_eq!(
|
|
135
|
+
p.read("foo.py"),
|
|
136
|
+
"while False:\n pass\nelse:\n print(\"b\")\n"
|
|
137
|
+
);
|
|
83
138
|
}
|
|
84
139
|
|
|
85
140
|
#[test]
|
|
@@ -44,7 +44,7 @@ fn class_attribute_reported_by_default_ignored_with_flag() {
|
|
|
44
44
|
let p1 = Project::new();
|
|
45
45
|
p1.write("foo.py", src);
|
|
46
46
|
let result = p1.run(&["foo.py", "--no-color"]).unwrap();
|
|
47
|
-
assert!(result.contains("
|
|
47
|
+
assert!(result.contains("DC06 Name `THING` is never used"));
|
|
48
48
|
|
|
49
49
|
p.write("foo.py", src);
|
|
50
50
|
assert_eq!(
|
|
@@ -82,3 +82,15 @@ fn unused_import_suppressed() {
|
|
|
82
82
|
p.write("foo.py", "from typing import Optional # noqa: DC07\n");
|
|
83
83
|
assert_eq!(p.run(&["foo.py", "--no-color"]), None);
|
|
84
84
|
}
|
|
85
|
+
|
|
86
|
+
#[test]
|
|
87
|
+
fn class_body_names_use_the_dc06_code_for_noqa() {
|
|
88
|
+
let p = Project::new();
|
|
89
|
+
p.write(
|
|
90
|
+
"foo.py",
|
|
91
|
+
"class Foo:\n kept = 1 # noqa: DC06\n reported = 2 # noqa: DC01\n\n\nFoo()\n",
|
|
92
|
+
);
|
|
93
|
+
let result = p.run(&["foo.py", "--no-color"]).unwrap();
|
|
94
|
+
assert!(!result.contains("`kept`"));
|
|
95
|
+
assert!(result.contains("foo.py:3:4: DC06 Name `reported` is never used"));
|
|
96
|
+
}
|