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.
Files changed (305) hide show
  1. {lapsed-3.2.1 → lapsed-3.2.2}/Cargo.lock +1 -1
  2. {lapsed-3.2.1 → lapsed-3.2.2}/Cargo.toml +1 -1
  3. {lapsed-3.2.1 → lapsed-3.2.2}/PKG-INFO +1 -1
  4. {lapsed-3.2.1 → lapsed-3.2.2}/pyproject.toml +1 -1
  5. {lapsed-3.2.1 → lapsed-3.2.2}/src/visitor/dead_code_visitor.rs +318 -43
  6. {lapsed-3.2.1 → lapsed-3.2.2}/tests/fix_special_cases.rs +60 -5
  7. {lapsed-3.2.1 → lapsed-3.2.2}/tests/ignore_flags_cli.rs +1 -1
  8. {lapsed-3.2.1 → lapsed-3.2.2}/tests/noqa_comments.rs +12 -0
  9. lapsed-3.2.2/tests/parity/known_differences.txt +27 -0
  10. {lapsed-3.2.1 → lapsed-3.2.2}/tests/pytest_and_typing.rs +54 -0
  11. lapsed-3.2.1/tests/parity/known_differences.txt +0 -36
  12. {lapsed-3.2.1 → lapsed-3.2.2}/.github/workflows/ci.yml +0 -0
  13. {lapsed-3.2.1 → lapsed-3.2.2}/.github/workflows/wheels.yml +0 -0
  14. {lapsed-3.2.1 → lapsed-3.2.2}/.gitignore +0 -0
  15. {lapsed-3.2.1 → lapsed-3.2.2}/.pre-commit-hooks.yaml +0 -0
  16. {lapsed-3.2.1 → lapsed-3.2.2}/AGENTS.md +0 -0
  17. {lapsed-3.2.1 → lapsed-3.2.2}/CLAUDE.md +0 -0
  18. {lapsed-3.2.1 → lapsed-3.2.2}/LICENSE +0 -0
  19. {lapsed-3.2.1 → lapsed-3.2.2}/README.md +0 -0
  20. {lapsed-3.2.1 → lapsed-3.2.2}/docs/_static/AGPLv3-license.svg +0 -0
  21. {lapsed-3.2.1 → lapsed-3.2.2}/docs/_static/deadcode-logo-readme.png +0 -0
  22. {lapsed-3.2.1 → lapsed-3.2.2}/enhancement_proposals/dep_0.md +0 -0
  23. {lapsed-3.2.1 → lapsed-3.2.2}/enhancement_proposals/dep_1.md +0 -0
  24. {lapsed-3.2.1 → lapsed-3.2.2}/enhancement_proposals/dep_2.md +0 -0
  25. {lapsed-3.2.1 → lapsed-3.2.2}/enhancement_proposals/dep_3.md +0 -0
  26. {lapsed-3.2.1 → lapsed-3.2.2}/examples/profile_stages.rs +0 -0
  27. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/.github/dependabot.yml +0 -0
  28. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/.github/workflows/check-deadcode-on-python310.yml +0 -0
  29. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/.github/workflows/test.yml +0 -0
  30. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/.github/workflows/vulnerability-audit.yml +0 -0
  31. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/.gitignore +0 -0
  32. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/.pre-commit-config.yaml +0 -0
  33. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/.pre-commit-hooks.yaml +0 -0
  34. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/.readthedocs.yaml +0 -0
  35. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/LICENSE +0 -0
  36. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/Makefile +0 -0
  37. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/README.md +0 -0
  38. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/codecov.yml +0 -0
  39. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/__init__.py +0 -0
  40. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/actions/__init__.py +0 -0
  41. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/actions/find_python_filenames.py +0 -0
  42. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/actions/find_unused_names.py +0 -0
  43. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/actions/fix_or_show_unused_code.py +0 -0
  44. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/actions/get_unused_import_parts.py +0 -0
  45. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/actions/get_unused_names_error_message.py +0 -0
  46. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/actions/merge_overlaping_file_parts.py +0 -0
  47. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/actions/parse_abstract_syntax_tree.py +0 -0
  48. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/actions/parse_arguments.py +0 -0
  49. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/actions/remove_file_parts_from_content.py +0 -0
  50. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/cli.py +0 -0
  51. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/constants.py +0 -0
  52. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/data_types.py +0 -0
  53. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/utils/__init__.py +0 -0
  54. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/utils/add_colors_to_diff.py +0 -0
  55. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/utils/fix_indent.py +0 -0
  56. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/utils/flatten_lists.py +0 -0
  57. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/utils/nested_scopes.py +0 -0
  58. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/utils/normalize_path.py +0 -0
  59. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/utils/print_ast.py +0 -0
  60. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/utils/print_safely.py +0 -0
  61. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/visitor/__init__.py +0 -0
  62. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/visitor/code_item.py +0 -0
  63. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/visitor/dead_code_visitor.py +0 -0
  64. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/visitor/ignore.py +0 -0
  65. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/visitor/lines.py +0 -0
  66. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/visitor/module_exports.py +0 -0
  67. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/visitor/noqa.py +0 -0
  68. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/visitor/usage_graph.py +0 -0
  69. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/deadcode/visitor/utils.py +0 -0
  70. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/_static/AGPLv3-license.svg +0 -0
  71. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/_static/custom.css +0 -0
  72. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/_static/deadcode-logo-readme.png +0 -0
  73. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/comparison.md +0 -0
  74. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/conf.py +0 -0
  75. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/contributing.md +0 -0
  76. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/dep.md +0 -0
  77. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/dep_1.md +0 -0
  78. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/dep_2.md +0 -0
  79. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/dep_3.md +0 -0
  80. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/dep_4.md +0 -0
  81. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/dep_5.md +0 -0
  82. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/dep_6.md +0 -0
  83. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/dep_7.md +0 -0
  84. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/django.md +0 -0
  85. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/fastapi.md +0 -0
  86. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/features.md +0 -0
  87. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/index.md +0 -0
  88. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/installation.md +0 -0
  89. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/known_limitations.md +0 -0
  90. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/open_source_experiment.md +0 -0
  91. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/performance.md +0 -0
  92. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/pre_commit.md +0 -0
  93. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/release_notes.md +0 -0
  94. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/requirements.txt +0 -0
  95. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/rules.md +0 -0
  96. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/rust.md +0 -0
  97. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/security.md +0 -0
  98. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/docs/usage.md +0 -0
  99. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/enhancement_proposals/dep_0.md +0 -0
  100. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/enhancement_proposals/dep_1.md +0 -0
  101. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/enhancement_proposals/dep_2.md +0 -0
  102. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/enhancement_proposals/dep_3.md +0 -0
  103. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/enhancement_proposals/dep_4.md +0 -0
  104. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/enhancement_proposals/dep_5.md +0 -0
  105. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/enhancement_proposals/dep_6.md +0 -0
  106. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/enhancement_proposals/dep_7.md +0 -0
  107. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/pyproject.toml +0 -0
  108. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/requirements-dev.txt +0 -0
  109. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/requirements.txt +0 -0
  110. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/actions/test_find_python_filenames.py +0 -0
  111. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/actions/test_get_unused_import_parts.py +0 -0
  112. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/actions/test_merge_overlaping_file_parts.py +0 -0
  113. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/actions/test_remove_file_parts_from_content.py +0 -0
  114. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli/test_cli_exit_status.py +0 -0
  115. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli/test_cli_module_entry_point.py +0 -0
  116. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli/test_cli_output_encoding.py +0 -0
  117. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli/test_deadcode.py +0 -0
  118. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli/test_output.py +0 -0
  119. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli/test_verbose_output_encoding.py +0 -0
  120. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/__init__.py +0 -0
  121. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_argument_routes.py +0 -0
  122. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_count.py +0 -0
  123. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_dry.py +0 -0
  124. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_exclude.py +0 -0
  125. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_fix.py +0 -0
  126. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_bodies_if_decorated_with.py +0 -0
  127. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_bodies_if_inherits_from.py +0 -0
  128. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_bodies_of.py +0 -0
  129. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_definitions.py +0 -0
  130. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_definitions_if_decorated_with.py +0 -0
  131. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_definitions_if_inherits_from.py +0 -0
  132. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_names.py +0 -0
  133. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_names_if_decorated_with.py +0 -0
  134. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_names_if_inherits_from.py +0 -0
  135. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_names_in_files.py +0 -0
  136. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_option_combinations.py +0 -0
  137. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_ignore_rules.py +0 -0
  138. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_no_color.py +0 -0
  139. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_only.py +0 -0
  140. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_parsing.py +0 -0
  141. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_pyproject_boolean_options.py +0 -0
  142. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_pyproject_list_options.py +0 -0
  143. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_quiet.py +0 -0
  144. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_verbose.py +0 -0
  145. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/cli_args/test_version.py +0 -0
  146. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/code_item/test_code_item.py +0 -0
  147. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/configuration/test_decorator_pyproject_configuration.py +0 -0
  148. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/conftest.py +0 -0
  149. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/documented_examples/test_django.py +0 -0
  150. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/documented_examples/test_documented_features.py +0 -0
  151. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/documented_examples/test_documented_options.py +0 -0
  152. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/documented_examples/test_documented_rules.py +0 -0
  153. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/documented_examples/test_fastapi.py +0 -0
  154. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/documented_examples/test_fastapi_dependency_injection.py +0 -0
  155. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/documented_examples/test_fastapi_documentation.py +0 -0
  156. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/documented_examples/test_pre_commit_hook_configuration.py +0 -0
  157. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/documented_examples/test_readme_coverage_badge.py +0 -0
  158. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/classes.py +0 -0
  159. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/admin.py +0 -0
  160. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/api.py +0 -0
  161. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/apps.py +0 -0
  162. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/forms.py +0 -0
  163. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/import_forms.py +0 -0
  164. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/management/commands/import_data.py +0 -0
  165. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/managers.py +0 -0
  166. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/middleware.py +0 -0
  167. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/migrations/0001_initial.py +0 -0
  168. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/models.py +0 -0
  169. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/settings.py +0 -0
  170. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/signals.py +0 -0
  171. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/templatetags/blog_tags.py +0 -0
  172. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/tests.py +0 -0
  173. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/urls.py +0 -0
  174. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/django/views.py +0 -0
  175. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi/dependencies.py +0 -0
  176. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi/main.py +0 -0
  177. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi/models.py +0 -0
  178. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi/routes/users.py +0 -0
  179. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi_dependency_injection/dependencies.py +0 -0
  180. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi_dependency_injection/main.py +0 -0
  181. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi_dependency_injection/models.py +0 -0
  182. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi_dependency_injection/security.py +0 -0
  183. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi_features/background.py +0 -0
  184. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi_features/errors.py +0 -0
  185. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi_features/main.py +0 -0
  186. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi_features/middleware.py +0 -0
  187. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi_features/models.py +0 -0
  188. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi_features/routes/items.py +0 -0
  189. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/fastapi_features/security.py +0 -0
  190. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/functions.py +0 -0
  191. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/subdir/another_subdir/__init__.py +0 -0
  192. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/subdir/one_more_file.py +0 -0
  193. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/files/variables.py +0 -0
  194. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/__init__.py +0 -0
  195. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/test_assign.py +0 -0
  196. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/test_attribute.py +0 -0
  197. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/test_cascading_dead_code.py +0 -0
  198. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/test_class_def.py +0 -0
  199. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/test_commented_out_code.py +0 -0
  200. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/test_empty_files.py +0 -0
  201. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/test_method_def.py +0 -0
  202. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/test_property.py +0 -0
  203. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/test_scope.py +0 -0
  204. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/test_unreachable_code.py +0 -0
  205. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/fix/test_unused_imports.py +0 -0
  206. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/nested_scope/test_nested_scope.py +0 -0
  207. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/noqa/test_noqa_comments.py +0 -0
  208. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/regression/__init__.py +0 -0
  209. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/regression/test_cascading_dead_code.py +0 -0
  210. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/regression/test_name_shadowing.py +0 -0
  211. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/regression/test_python314_attribute_error.py +0 -0
  212. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/utils/__init__.py +0 -0
  213. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/utils/test_add_colors_to_diff.py +0 -0
  214. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/utils/test_fix_indent.py +0 -0
  215. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/utils/test_normalize_path.py +0 -0
  216. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/utils/test_print_ast.py +0 -0
  217. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/utils/test_print_safely.py +0 -0
  218. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/__init__.py +0 -0
  219. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_accurate_usage_detection.py +0 -0
  220. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_commented_out_code.py +0 -0
  221. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_decorator_without_id_attribute.py +0 -0
  222. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_django_lifecycle_methods.py +0 -0
  223. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_ignore.py +0 -0
  224. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_ignore_interface_decorators.py +0 -0
  225. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_imports.py +0 -0
  226. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_line_number_detection.py +0 -0
  227. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_middleware_registration.py +0 -0
  228. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_name_usage_and_unreachable_detection.py +0 -0
  229. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_qualified_name_resolution.py +0 -0
  230. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_unittest_lifecycle_methods.py +0 -0
  231. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_utils.py +0 -0
  232. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_variable_parsing_from_ast_dump.py +0 -0
  233. {lapsed-3.2.1 → lapsed-3.2.2}/legacy-python/tests/visitor/test_wildcard_imports.py +0 -0
  234. {lapsed-3.2.1 → lapsed-3.2.2}/scripts/check.py +0 -0
  235. {lapsed-3.2.1 → lapsed-3.2.2}/scripts/parity.py +0 -0
  236. {lapsed-3.2.1 → lapsed-3.2.2}/src/actions/find_python_filenames.rs +0 -0
  237. {lapsed-3.2.1 → lapsed-3.2.2}/src/actions/fix_or_show_unused_code.rs +0 -0
  238. {lapsed-3.2.1 → lapsed-3.2.2}/src/actions/get_unused_import_parts.rs +0 -0
  239. {lapsed-3.2.1 → lapsed-3.2.2}/src/actions/get_unused_names_error_message.rs +0 -0
  240. {lapsed-3.2.1 → lapsed-3.2.2}/src/actions/merge_overlapping_file_parts.rs +0 -0
  241. {lapsed-3.2.1 → lapsed-3.2.2}/src/actions/parse_abstract_syntax_tree.rs +0 -0
  242. {lapsed-3.2.1 → lapsed-3.2.2}/src/actions/parse_arguments.rs +0 -0
  243. {lapsed-3.2.1 → lapsed-3.2.2}/src/actions/parse_tach_config.rs +0 -0
  244. {lapsed-3.2.1 → lapsed-3.2.2}/src/actions/remove_file_parts_from_content.rs +0 -0
  245. {lapsed-3.2.1 → lapsed-3.2.2}/src/cli.rs +0 -0
  246. {lapsed-3.2.1 → lapsed-3.2.2}/src/constants.rs +0 -0
  247. {lapsed-3.2.1 → lapsed-3.2.2}/src/data_types.rs +0 -0
  248. {lapsed-3.2.1 → lapsed-3.2.2}/src/lib.rs +0 -0
  249. {lapsed-3.2.1 → lapsed-3.2.2}/src/main.rs +0 -0
  250. {lapsed-3.2.1 → lapsed-3.2.2}/src/utils/add_colors_to_diff.rs +0 -0
  251. {lapsed-3.2.1 → lapsed-3.2.2}/src/utils/fix_indent.rs +0 -0
  252. {lapsed-3.2.1 → lapsed-3.2.2}/src/utils/fnmatch.rs +0 -0
  253. {lapsed-3.2.1 → lapsed-3.2.2}/src/utils/line_index.rs +0 -0
  254. {lapsed-3.2.1 → lapsed-3.2.2}/src/utils/nested_scopes.rs +0 -0
  255. {lapsed-3.2.1 → lapsed-3.2.2}/src/utils/path_utils.rs +0 -0
  256. {lapsed-3.2.1 → lapsed-3.2.2}/src/visitor/code_item.rs +0 -0
  257. {lapsed-3.2.1 → lapsed-3.2.2}/src/visitor/ignore.rs +0 -0
  258. {lapsed-3.2.1 → lapsed-3.2.2}/src/visitor/noqa.rs +0 -0
  259. {lapsed-3.2.1 → lapsed-3.2.2}/src/visitor/utils.rs +0 -0
  260. {lapsed-3.2.1 → lapsed-3.2.2}/tests/aspirational_unimplemented.rs +0 -0
  261. {lapsed-3.2.1 → lapsed-3.2.2}/tests/attrs_field_hooks.rs +0 -0
  262. {lapsed-3.2.1 → lapsed-3.2.2}/tests/common/mod.rs +0 -0
  263. {lapsed-3.2.1 → lapsed-3.2.2}/tests/deadcode_integration.rs +0 -0
  264. {lapsed-3.2.1 → lapsed-3.2.2}/tests/exit_code.rs +0 -0
  265. {lapsed-3.2.1 → lapsed-3.2.2}/tests/files/classes.py +0 -0
  266. {lapsed-3.2.1 → lapsed-3.2.2}/tests/files/functions.py +0 -0
  267. {lapsed-3.2.1 → lapsed-3.2.2}/tests/files/subdir/another_subdir/__init__.py +0 -0
  268. {lapsed-3.2.1 → lapsed-3.2.2}/tests/files/subdir/one_more_file.py +0 -0
  269. {lapsed-3.2.1 → lapsed-3.2.2}/tests/files/variables.py +0 -0
  270. {lapsed-3.2.1 → lapsed-3.2.2}/tests/fix_cli_option.rs +0 -0
  271. {lapsed-3.2.1 → lapsed-3.2.2}/tests/imports_and_output.rs +0 -0
  272. {lapsed-3.2.1 → lapsed-3.2.2}/tests/line_number_detection.rs +0 -0
  273. {lapsed-3.2.1 → lapsed-3.2.2}/tests/modern_python_syntax.rs +0 -0
  274. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/absimport/main.py +0 -0
  275. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/absimport/pkg/__init__.py +0 -0
  276. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/absimport/pkg/a.py +0 -0
  277. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/abstract/m.py +0 -0
  278. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/cascade/m.py +0 -0
  279. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/dc06/m.py +0 -0
  280. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/dc09/m.py +0 -0
  281. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/dc09b/m.py +0 -0
  282. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/dc12/m.py +0 -0
  283. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/dc13/m.py +0 -0
  284. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/django/m.py +0 -0
  285. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/django_mw/proj/middleware.py +0 -0
  286. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/django_mw/proj/settings.py +0 -0
  287. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/dotted_base/m.py +0 -0
  288. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/fastapi/m.py +0 -0
  289. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/fix_edge_cases/m.py +0 -0
  290. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/imports_fix/m.py +0 -0
  291. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/multiline_assign/m.py +0 -0
  292. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/noqa_decorated/m.py +0 -0
  293. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/pyproject_options/m.py +0 -0
  294. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/pyproject_options/pyproject.toml +0 -0
  295. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/quoted/m.py +0 -0
  296. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/samename/a.py +0 -0
  297. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/samename/b.py +0 -0
  298. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/samename/c.py +0 -0
  299. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/shadowing/m.py +0 -0
  300. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/typecomment/m.py +0 -0
  301. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/unicode/m.py +0 -0
  302. {lapsed-3.2.1 → lapsed-3.2.2}/tests/parity/cases/wildcard/m.py +0 -0
  303. {lapsed-3.2.1 → lapsed-3.2.2}/tests/scope_tracking.rs +0 -0
  304. {lapsed-3.2.1 → lapsed-3.2.2}/tests/tach_config_cli.rs +0 -0
  305. {lapsed-3.2.1 → lapsed-3.2.2}/tests/version_consistency.rs +0 -0
@@ -395,7 +395,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
395
395
 
396
396
  [[package]]
397
397
  name = "lapsed"
398
- version = "3.2.1"
398
+ version = "3.2.2"
399
399
  dependencies = [
400
400
  "clap",
401
401
  "once_cell",
@@ -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.1"
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."
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lapsed
3
- Version: 3.2.1
3
+ Version: 3.2.2
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: License :: OSI Approved :: GNU Affero General Public License v3
6
6
  Classifier: Natural Language :: English
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "lapsed"
7
- version = "3.2.1"
7
+ version = "3.2.2"
8
8
  description = "Find and remove dead code in Python codebases (Rust implementation)."
9
9
  readme = "README.md"
10
10
  license = { text = "AGPL-3.0-only" }
@@ -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
- fn add_unreachable_body(&mut self, name: &str, range: TextRange, message: &str) {
362
- let (line, column) = self.line_index.line_col(range.start());
363
- let (end_line, end_column) = self.line_index.line_col(range.end());
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
- || noqa::ignore_line(&self.noqa_lines, line as u32, "DC09")
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
- vec![Part::new(
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
- self.push_definition(DefinedKind::Variable, name, start, end, &[], ignored, None);
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.walk_expr(returns);
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.walk_expr(&a.annotation);
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) => self.add_unreachable_body(
1262
- "while",
1263
- s.range,
1264
- "Unsatisfiable `while` condition",
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
- self.add_unreachable_body(
1268
- "else",
1269
- s.orelse[0].range(),
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
- match Self::conditional_outcome(&s.test) {
1281
- Some(false) => {
1282
- self.add_unreachable_body("if", s.range, "Unsatisfiable `if` condition")
1283
- }
1284
- Some(true) => {
1285
- if let Some(first_clause) = s.elif_else_clauses.first() {
1286
- self.add_unreachable_body(
1287
- "else",
1288
- first_clause.body[0].range(),
1289
- "Unreachable `else` block",
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.walk_expr(annotation);
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) = &parameters.vararg {
1389
1633
  if let Some(annotation) = &vararg.annotation {
1390
- self.walk_expr(annotation);
1634
+ self.walk_annotation(annotation);
1391
1635
  }
1392
1636
  }
1393
1637
  if let Some(kwarg) = &parameters.kwarg {
1394
1638
  if let Some(annotation) = &kwarg.annotation {
1395
- self.walk_expr(annotation);
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 false_if_removes_the_full_statement() {
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 Unsatisfiable `if` condition"));
59
- assert!(!p.exists("foo.py"));
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 false_while_removes_the_full_statement() {
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
- assert!(!p.exists("foo.py"));
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("DC01 Variable `THING`"));
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
+ }