sarj-python-lint 1.27.4__tar.gz → 1.28.0__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 (192) hide show
  1. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/interpreter_argv.py +48 -14
  4. sarj_python_lint-1.28.0/src/sarj_python_lint/rules/_closed_helpers.py +100 -0
  5. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_registry.py +9 -0
  6. sarj_python_lint-1.28.0/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +213 -0
  7. sarj_python_lint-1.28.0/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +260 -0
  8. sarj_python_lint-1.28.0/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +284 -0
  9. sarj_python_lint-1.28.0/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +383 -0
  10. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +68 -9
  11. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/.gitignore +0 -0
  12. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/LICENSE +0 -0
  13. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/README.md +0 -0
  14. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/__init__.py +0 -0
  15. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/__main__.py +0 -0
  16. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  17. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/_file_context.py +0 -0
  18. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/_filesystem.py +0 -0
  19. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/_python_target.py +0 -0
  20. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  21. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/_secret_names.py +0 -0
  22. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/_source.py +0 -0
  23. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/_version.py +0 -0
  24. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/benchmark.py +0 -0
  25. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/json_boundary.py +0 -0
  26. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/py.typed +0 -0
  27. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/ratchet.py +0 -0
  28. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rule_base.py +0 -0
  29. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  30. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  31. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  32. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  33. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  34. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  35. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  36. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  37. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  38. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  39. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  40. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  41. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  42. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  43. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  44. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  45. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  46. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  47. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  48. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  49. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  50. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  51. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  52. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  53. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  54. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  55. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  56. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  57. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  58. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  59. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  60. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  61. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  62. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  63. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  64. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  65. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  66. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  67. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  68. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  69. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  70. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  71. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  72. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  73. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  74. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  75. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  76. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  77. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  78. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  79. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  80. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  81. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  82. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  83. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  84. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  85. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  86. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  87. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  88. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  89. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  90. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  91. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  92. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  93. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  94. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  95. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  96. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  97. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  98. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  99. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  100. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  101. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  102. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  103. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  104. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  105. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  106. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  107. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  108. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  109. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  110. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  111. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  112. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  113. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  114. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  115. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  116. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  117. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  118. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  119. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  120. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  121. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  122. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  123. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  124. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  125. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  126. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  127. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  128. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  129. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  130. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  131. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  132. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  133. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  134. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  135. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  136. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  137. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  138. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  139. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  140. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  141. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  142. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  143. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  144. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  145. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  146. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  147. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  148. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  149. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  150. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  151. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  152. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  153. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  154. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  155. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  156. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  157. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  158. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  159. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  160. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  161. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  162. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  163. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  164. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  165. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  166. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  167. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  168. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  169. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  170. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  171. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  172. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  173. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  174. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  175. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  176. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  177. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  178. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  179. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  180. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  181. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  182. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  183. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  184. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  185. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  186. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  187. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  188. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  189. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  190. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  191. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  192. {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.27.4
3
+ Version: 1.28.0
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
6
6
  Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.27.4"
3
+ version = "1.28.0"
4
4
  description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "sarj-ai" }]
@@ -29,6 +29,7 @@ class ParsedOption:
29
29
  invocation: InterpreterInvocation | None = None
30
30
  consumed: int = 1
31
31
  stdin_mode: bool = False
32
+ command_mode: bool = False
32
33
 
33
34
 
34
35
  _SHELLS = frozenset({"sh", "bash", "dash", "zsh", "ksh"})
@@ -112,6 +113,7 @@ _WRAPPER_VALUES = MappingProxyType(
112
113
  }
113
114
  ),
114
115
  "timeout": frozenset({"-s", "--signal", "-k", "--kill-after"}),
116
+ "dumb-init": frozenset(),
115
117
  }
116
118
  )
117
119
  _MAX_WRAPPERS = 8
@@ -136,6 +138,8 @@ def unwrap_command(
136
138
  continue
137
139
  if command not in _WRAPPER_VALUES or (allowed_wrappers is not None and command not in allowed_wrappers):
138
140
  return current
141
+ # dumb-init uses execvp; its child cannot be a shell builtin.
142
+ allow_shell_builtins = allow_shell_builtins and command != "dumb-init"
139
143
  current = current[_value_wrapper_index(current, command) :]
140
144
  msg = "command wrapper nesting exceeds analysis bound"
141
145
  raise UnprovableCommandError(msg)
@@ -160,6 +164,8 @@ def _command_wrapper_index(arguments: Sequence[str], command: str) -> int:
160
164
  def _value_wrapper_index(arguments: Sequence[str], command: str) -> int:
161
165
  if command == "env":
162
166
  return _env_wrapper_index(arguments)
167
+ if command == "dumb-init":
168
+ return _dumb_init_wrapper_index(arguments)
163
169
  index = 1
164
170
  while index < len(arguments):
165
171
  argument = arguments[index]
@@ -173,6 +179,24 @@ def _value_wrapper_index(arguments: Sequence[str], command: str) -> int:
173
179
  return index + int(command == "timeout")
174
180
 
175
181
 
182
+ def _dumb_init_wrapper_index(arguments: Sequence[str]) -> int:
183
+ index = 1
184
+ while index < len(arguments):
185
+ argument = arguments[index]
186
+ if argument in {"-h", "--help", "-V", "--version"}:
187
+ return len(arguments)
188
+ if argument == "--":
189
+ index += 1
190
+ break
191
+ if not argument.startswith("-") or argument == "-":
192
+ break
193
+ if argument not in {"-c", "--single-child", "-v", "--verbose"}:
194
+ msg = "unsupported dumb-init wrapper option"
195
+ raise UnprovableCommandError(msg)
196
+ index += 1
197
+ return index
198
+
199
+
176
200
  def _env_wrapper_index(arguments: Sequence[str]) -> int:
177
201
  index = 1
178
202
  while index < len(arguments) and arguments[index].startswith("-"):
@@ -237,7 +261,7 @@ def classify_interpreter(argv: Sequence[str]) -> InterpreterInvocation:
237
261
  return _python(arguments[1:])
238
262
  if executable in _SHELLS:
239
263
  return _short_options(
240
- arguments[1:], source="c", values=frozenset({"o", "O"}), shell_mode=True, bash_mode=executable == "bash"
264
+ arguments[1:], source="c", values=frozenset({"o", "O"}), shell_mode=True, shell_name=executable
241
265
  )
242
266
  if executable in {"node", "nodejs"}:
243
267
  return _node(arguments[1:])
@@ -342,13 +366,21 @@ def _ruby_long_option(argument: str, following: str | None) -> ParsedOption:
342
366
 
343
367
 
344
368
  def _short_options(
345
- arguments: Sequence[str], *, source: str, values: frozenset[str], shell_mode: bool = False, bash_mode: bool = False
369
+ arguments: Sequence[str], *, source: str, values: frozenset[str], shell_mode: bool = False, shell_name: str = ""
346
370
  ) -> InterpreterInvocation:
347
371
  index = 0
348
372
  stdin_mode = False
373
+ command_mode = False
349
374
  short_seen = False
375
+ bash_mode = shell_name == "bash"
376
+ validated_shell = shell_name in {"bash", "sh", "dash"}
350
377
  while index < len(arguments):
351
378
  boundary = _external_boundary(arguments, index, plus_options=True)
379
+ if boundary is not None and command_mode:
380
+ index += int(arguments[index] in {"-", "--"})
381
+ if index >= len(arguments):
382
+ return InterpreterInvocation("unknown")
383
+ return InterpreterInvocation("shell", arguments[index], tuple(arguments[index + 2 :]))
352
384
  if boundary is not None:
353
385
  return InterpreterInvocation("stdin") if stdin_mode else boundary
354
386
  argument = arguments[index]
@@ -357,13 +389,14 @@ def _short_options(
357
389
  else:
358
390
  short_seen = True
359
391
  parsed = _short_option(
360
- arguments, index, source=source, values=values, shell_mode=shell_mode, bash_mode=bash_mode
392
+ arguments, index, source=source, values=values, shell_mode=shell_mode, validated_shell=validated_shell
361
393
  )
362
394
  if parsed.invocation is not None:
363
395
  return parsed.invocation
364
396
  stdin_mode = stdin_mode or parsed.stdin_mode
397
+ command_mode = command_mode or parsed.command_mode
365
398
  index += parsed.consumed
366
- return InterpreterInvocation("stdin")
399
+ return InterpreterInvocation("unknown" if command_mode else "stdin")
367
400
 
368
401
 
369
402
  def _long_option(
@@ -395,18 +428,23 @@ def _short_option(
395
428
  source: str,
396
429
  values: frozenset[str],
397
430
  shell_mode: bool,
398
- bash_mode: bool = False,
431
+ validated_shell: bool = False,
399
432
  ) -> ParsedOption:
400
- if bash_mode:
433
+ if validated_shell:
401
434
  return _shell_short_option(arguments, index, values)
402
435
  argument = arguments[index]
403
436
  for cursor, flag in enumerate(argument[1:], 1):
404
437
  if flag in source:
405
- attached = "" if shell_mode else argument[cursor + 1 :]
438
+ if shell_mode:
439
+ if not values.isdisjoint(argument[cursor + 1 :]):
440
+ return ParsedOption(InterpreterInvocation("unknown"))
441
+ return ParsedOption(command_mode=True)
442
+ attached = argument[cursor + 1 :]
406
443
  operand = attached or _following_operand(arguments, index)
407
- forwarded = tuple(arguments[index + 3 :]) if shell_mode else ()
408
- return ParsedOption(InterpreterInvocation("shell" if shell_mode else "inline", operand, forwarded))
444
+ return ParsedOption(InterpreterInvocation("inline", operand))
409
445
  if flag in values:
446
+ if shell_mode and (flag == "O" or argument[cursor + 1 :] == "c"):
447
+ return ParsedOption(InterpreterInvocation("unknown"))
410
448
  return ParsedOption(consumed=1 + int(cursor + 1 == len(argument)))
411
449
  return ParsedOption()
412
450
 
@@ -424,11 +462,7 @@ def _shell_short_option(arguments: Sequence[str], index: int, values: frozenset[
424
462
  if flag == "O" or operand not in _SHELL_NAMED_OPTIONS:
425
463
  return ParsedOption(InterpreterInvocation("unknown"))
426
464
  consumed += 1
427
- if "c" in flags:
428
- operand = _following_operand(arguments, index + consumed - 1)
429
- forwarded = tuple(arguments[index + consumed + 2 :])
430
- return ParsedOption(InterpreterInvocation("shell", operand, forwarded))
431
- return ParsedOption(consumed=consumed, stdin_mode="s" in flags)
465
+ return ParsedOption(consumed=consumed, stdin_mode="s" in flags, command_mode="c" in flags)
432
466
 
433
467
 
434
468
  def _node(arguments: Sequence[str]) -> InterpreterInvocation:
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from typing import TYPE_CHECKING
5
+
6
+
7
+ if TYPE_CHECKING:
8
+ from sarj_python_lint._file_context import PythonFileContext
9
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance
10
+
11
+
12
+ _DYNAMIC_NAMES = frozenset(
13
+ {"globals", "locals", "vars", "eval", "exec", "getattr", "setattr", "delattr", "__import__", "__all__"}
14
+ )
15
+
16
+
17
+ def dynamic_namespace(context: PythonFileContext) -> bool:
18
+ return (
19
+ any(node.id in _DYNAMIC_NAMES for node in context.nodes(ast.Name))
20
+ or any(node.attr in _DYNAMIC_NAMES | {"__dict__"} for node in context.nodes(ast.Attribute))
21
+ or any(node.name == "*" for node in context.nodes(ast.alias))
22
+ )
23
+
24
+
25
+ def executable_body(function: ast.FunctionDef) -> list[ast.stmt]:
26
+ body = function.body
27
+ return (
28
+ body[1:]
29
+ if body
30
+ and isinstance(body[0], ast.Expr)
31
+ and isinstance(body[0].value, ast.Constant)
32
+ and isinstance(body[0].value.value, str)
33
+ else body
34
+ )
35
+
36
+
37
+ def closed_references(
38
+ context: PythonFileContext, function: ast.FunctionDef, provenance: ResourceProvenance
39
+ ) -> list[ast.Name] | None:
40
+ if (
41
+ context.parents.get(function) is not context.tree
42
+ or not function.name.startswith("_")
43
+ or function.name.startswith("__")
44
+ or function.decorator_list
45
+ or function.type_params
46
+ ):
47
+ return None
48
+ if (
49
+ function.args.vararg is not None
50
+ or function.args.kwarg is not None
51
+ or dynamic_namespace(context)
52
+ or any(node.attr == function.name for node in context.nodes(ast.Attribute))
53
+ or any(
54
+ isinstance(node.value, str) and function.name in node.value.split(".")
55
+ for node in context.nodes(ast.Constant)
56
+ )
57
+ ):
58
+ return None
59
+ references = [node for node in context.nodes(ast.Name) if node.id == function.name]
60
+ if not references or any(
61
+ not isinstance(node.ctx, ast.Load) or provenance.binding(node.id, node) is not function for node in references
62
+ ):
63
+ return None
64
+ return references
65
+
66
+
67
+ def direct_call(reference: ast.Name, context: PythonFileContext) -> ast.Call | None:
68
+ parent = context.parents.get(reference)
69
+ if not isinstance(parent, ast.Call) or parent.func is not reference:
70
+ return None
71
+ if any(isinstance(argument, ast.Starred) for argument in parent.args) or any(
72
+ keyword.arg is None for keyword in parent.keywords
73
+ ):
74
+ return None
75
+ return parent
76
+
77
+
78
+ def call_matches(function: ast.FunctionDef, call: ast.Call) -> bool:
79
+ positional = [*function.args.posonlyargs, *function.args.args]
80
+ keywords = {arg.arg for arg in (*function.args.args, *function.args.kwonlyargs)}
81
+ required = {arg.arg for arg in positional[: len(positional) - len(function.args.defaults)]}
82
+ required.update(
83
+ arg.arg
84
+ for arg, default in zip(function.args.kwonlyargs, function.args.kw_defaults, strict=True)
85
+ if default is None
86
+ )
87
+ supplied = {arg.arg for arg in positional[: len(call.args)]}
88
+ if len(call.args) > len(positional):
89
+ return False
90
+ for keyword in call.keywords:
91
+ if keyword.arg not in keywords or keyword.arg in supplied:
92
+ return False
93
+ supplied.add(keyword.arg)
94
+ return required <= supplied
95
+
96
+
97
+ def stable_class(context: PythonFileContext, declaration: ast.ClassDef, provenance: ResourceProvenance) -> bool:
98
+ if declaration.decorator_list or declaration.keywords or context.parents.get(declaration) is not context.tree:
99
+ return False
100
+ return provenance.bindings.get(context.tree, {}).get(declaration.name) == [declaration]
@@ -207,6 +207,11 @@ if TYPE_CHECKING:
207
207
  from collections.abc import Mapping
208
208
 
209
209
  from sarj_python_lint.rule_base import Rule
210
+ from sarj_python_lint.rules.duplicate_adjacent_exception_handler import DuplicateAdjacentExceptionHandler
211
+ from sarj_python_lint.rules.duplicate_closed_pure_helper import DuplicateClosedPureHelper
212
+ from sarj_python_lint.rules.redundant_test_constructor_forwarder import RedundantTestConstructorForwarder
213
+ from sarj_python_lint.rules.repeated_unimplemented_test_contract import RepeatedUnimplementedTestContract
214
+
210
215
 
211
216
  REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
212
217
  {
@@ -348,5 +353,9 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
348
353
  PreferRegexFullmatch.id: PreferRegexFullmatch,
349
354
  PreferMonotonicForElapsedTime.id: PreferMonotonicForElapsedTime,
350
355
  TypedErrorReasons.id: TypedErrorReasons,
356
+ RedundantTestConstructorForwarder.id: RedundantTestConstructorForwarder,
357
+ DuplicateAdjacentExceptionHandler.id: DuplicateAdjacentExceptionHandler,
358
+ DuplicateClosedPureHelper.id: DuplicateClosedPureHelper,
359
+ RepeatedUnimplementedTestContract.id: RepeatedUnimplementedTestContract,
351
360
  }
352
361
  )
@@ -0,0 +1,213 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import builtins
5
+ from pathlib import PurePosixPath
6
+ from typing import (
7
+ TYPE_CHECKING,
8
+ ClassVar,
9
+ cast, # ruff: ignore[banned-api] -- vars exposes Any; the object view permits isinstance narrowing.
10
+ final,
11
+ override,
12
+ )
13
+
14
+ from sarj_python_lint.rule_base import (
15
+ AutofixPolicy,
16
+ Diagnostic,
17
+ ExampleFile,
18
+ ExampleOutcome,
19
+ ProjectRule,
20
+ RuleCategory,
21
+ RuleDocumentation,
22
+ RuleExample,
23
+ Severity,
24
+ is_suppressed,
25
+ )
26
+ from sarj_python_lint.rules._closed_helpers import dynamic_namespace, stable_class
27
+ from sarj_python_lint.rules._paths import is_generated
28
+ from sarj_python_lint.rules._project_index import ProjectIndexSet
29
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance
30
+ from sarj_python_lint.rules._suppression_comments import scan_comments
31
+
32
+
33
+ if TYPE_CHECKING:
34
+ from collections.abc import Mapping
35
+
36
+ from sarj_python_lint._file_context import PythonFileContext
37
+ from sarj_python_lint.rules._project_index import SourceUnit, SymbolRef
38
+ from sarj_python_lint.rules._suppression_comments import Comment
39
+
40
+
41
+ _BUILTIN_NAMESPACE = cast("Mapping[str, object]", vars(builtins))
42
+ _BUILTIN_EXCEPTIONS = frozenset(
43
+ name for name, value in _BUILTIN_NAMESPACE.items() if isinstance(value, type) and issubclass(value, Exception)
44
+ )
45
+ _BAD = "try:\n action()\nexcept ValueError as error:\n raise RuntimeError('invalid') from error\nexcept TypeError as error:\n raise RuntimeError('invalid') from error\n"
46
+ _GOOD = "try:\n action()\nexcept (ValueError, TypeError) as error:\n raise RuntimeError('invalid') from error\n"
47
+
48
+
49
+ @final
50
+ class DuplicateAdjacentExceptionHandler(ProjectRule):
51
+ id = "duplicate-adjacent-exception-handler"
52
+ code = "SARJ488"
53
+ documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
54
+ default_level=Severity.WARNING,
55
+ summary="Adjacent exception handlers repeat the same executable translation.",
56
+ rationale="Repeating one translation for adjacent statically known exception classes creates extra branches to maintain without different behavior.",
57
+ remediation="Consider one tuple handler for the adjacent exception types. Preserve distinct messages, cleanup, ordering, and intentional independently maintained translations.",
58
+ category=RuleCategory.MAINTAINABILITY,
59
+ autofix=AutofixPolicy.NONE,
60
+ limitations=(
61
+ "Only ordinary try statements with adjacent handlers, identical exception bindings, executable ASTs and comments are compared; exception groups and bare re-raise/pass handlers are excluded.",
62
+ "Handler types must be simple names or tuples of unshadowed builtin or statically resolved first-party Exception subclasses, with unconditional stable declarations/imports.",
63
+ "Dynamic namespace access, unknown external exception classes, generated/vendor sources, different comments, and intervening handlers are excluded. No autofix preserves intentional translation seams.",
64
+ ),
65
+ examples=tuple(
66
+ RuleExample(
67
+ example_id=name,
68
+ title=title,
69
+ outcome=outcome,
70
+ files=(ExampleFile.python("app/service.py", source),),
71
+ focus_path=PurePosixPath("app/service.py"),
72
+ expected_count=count,
73
+ public=True,
74
+ )
75
+ for name, title, outcome, source, count in (
76
+ ("repeated-translation", "Adjacent handlers translate identically", ExampleOutcome.MATCH, _BAD, 1),
77
+ ("one-tuple-handler", "One handler owns the shared translation", ExampleOutcome.NO_MATCH, _GOOD, 0),
78
+ )
79
+ ),
80
+ )
81
+ description = documentation.summary
82
+
83
+ @override
84
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
85
+ tree = context.tree
86
+ if context.generated or tree is None or "except" not in context.symbol_source or dynamic_namespace(context):
87
+ return []
88
+ project = context.session.project or ProjectIndexSet.single(context.path, context.source)
89
+ unit = project.unit_or_source(context.path, context.source, tree)
90
+ provenance = ResourceProvenance(context)
91
+ comments = scan_comments(context.source)
92
+ findings: list[Diagnostic] = []
93
+ for statement in context.nodes(ast.Try):
94
+ previous: ast.ExceptHandler | None = None
95
+ reported = False
96
+ for handler in statement.handlers:
97
+ safe = _safe_types(handler.type, context, provenance, project, unit, seen=set())
98
+ if previous is not None and safe and _same_translation(previous, handler, comments):
99
+ if not reported and not is_suppressed(context.source_lines, handler.lineno, self.code):
100
+ findings.append(
101
+ Diagnostic(
102
+ path=context.path,
103
+ line=handler.lineno,
104
+ col=handler.col_offset + 1,
105
+ code=self.code,
106
+ severity=Severity.WARNING,
107
+ message="Adjacent exception handlers have identical translations; consider one tuple handler, preserving intentional separate error contracts.",
108
+ )
109
+ )
110
+ reported = True
111
+ else:
112
+ reported = False
113
+ previous = handler if safe else None
114
+ return sorted(findings, key=lambda finding: (finding.line, finding.col))
115
+
116
+
117
+ def _same_translation(first: ast.ExceptHandler, second: ast.ExceptHandler, comments: list[Comment]) -> bool:
118
+ if first.name != second.name or not first.body or first.type is None or second.type is None:
119
+ return False
120
+ if not {node.id for node in ast.walk(first.type) if isinstance(node, ast.Name)}.isdisjoint(
121
+ node.id for node in ast.walk(second.type) if isinstance(node, ast.Name)
122
+ ):
123
+ return False
124
+ if len(first.body) == 1 and (
125
+ isinstance(first.body[0], ast.Pass) or (isinstance(first.body[0], ast.Raise) and first.body[0].exc is None)
126
+ ):
127
+ return False
128
+ if ast.dump(ast.Module(body=first.body, type_ignores=[])) != ast.dump(
129
+ ast.Module(body=second.body, type_ignores=[])
130
+ ):
131
+ return False
132
+ if any((first.end_lineno or first.lineno) < comment.line < second.lineno for comment in comments):
133
+ return False
134
+ return _comments(first, comments) == _comments(second, comments)
135
+
136
+
137
+ def _comments(handler: ast.ExceptHandler, comments: list[Comment]) -> list[str]:
138
+ return [
139
+ comment.body for comment in comments if handler.lineno <= comment.line <= (handler.end_lineno or handler.lineno)
140
+ ]
141
+
142
+
143
+ def _safe_types(
144
+ expression: ast.expr | None,
145
+ context: PythonFileContext,
146
+ provenance: ResourceProvenance,
147
+ project: ProjectIndexSet,
148
+ unit: SourceUnit | None,
149
+ *,
150
+ seen: set[SymbolRef],
151
+ ) -> bool:
152
+ if isinstance(expression, ast.Tuple):
153
+ return bool(expression.elts) and all(
154
+ _safe_types(element, context, provenance, project, unit, seen=seen) for element in expression.elts
155
+ )
156
+ if not isinstance(expression, ast.Name):
157
+ return False
158
+ if (
159
+ expression.id in _BUILTIN_EXCEPTIONS
160
+ and context.imports.builtin_is_unshadowed(expression.id)
161
+ and provenance.binding(expression.id, expression) is None
162
+ ):
163
+ return True
164
+ return _first_party_exception(expression, context, provenance, project, unit, seen=seen)
165
+
166
+
167
+ def _first_party_exception(
168
+ expression: ast.Name,
169
+ context: PythonFileContext,
170
+ provenance: ResourceProvenance,
171
+ project: ProjectIndexSet,
172
+ unit: SourceUnit | None,
173
+ *,
174
+ seen: set[SymbolRef],
175
+ ) -> bool:
176
+ from sarj_python_lint._file_context import PythonFileContext # ruff: ignore[import-outside-top-level] -- defer until the rule registry finishes loading.
177
+
178
+ binding = provenance.binding(expression.id, expression)
179
+ if not isinstance(binding, ast.ClassDef | ast.alias) or unit is None:
180
+ return False
181
+ if isinstance(binding, ast.alias):
182
+ statement = context.parents.get(binding)
183
+ if statement is None or context.parents.get(statement) is not context.tree:
184
+ return False
185
+ summary = project.class_for(unit, expression)
186
+ if (
187
+ summary is None
188
+ or summary.symbol in seen
189
+ or (owner := project.source_unit(summary.symbol.module)) is None
190
+ or owner.tree is None
191
+ or is_generated(owner.path, owner.source)
192
+ ):
193
+ return False
194
+ definition_context = (
195
+ context if owner.path == context.path else PythonFileContext(owner.path, owner.source, context.session)
196
+ )
197
+ definition_provenance = provenance if definition_context is context else ResourceProvenance(definition_context)
198
+ declaration = next(
199
+ (node for node in definition_context.nodes(ast.ClassDef) if node.name == summary.symbol.name), None
200
+ )
201
+ return (
202
+ declaration is not None
203
+ and stable_class(definition_context, declaration, definition_provenance)
204
+ and len(declaration.bases) == 1
205
+ and _safe_types(
206
+ declaration.bases[0],
207
+ definition_context,
208
+ definition_provenance,
209
+ project,
210
+ owner,
211
+ seen=seen | {summary.symbol},
212
+ )
213
+ )