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.
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/interpreter_argv.py +48 -14
- sarj_python_lint-1.28.0/src/sarj_python_lint/rules/_closed_helpers.py +100 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_registry.py +9 -0
- sarj_python_lint-1.28.0/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +213 -0
- sarj_python_lint-1.28.0/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +260 -0
- sarj_python_lint-1.28.0/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +284 -0
- sarj_python_lint-1.28.0/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +383 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +68 -9
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/.gitignore +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/LICENSE +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/README.md +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {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
- {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
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {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
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {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
- {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
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {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
- {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
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {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
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {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
- {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
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {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
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {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
- {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
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {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
- {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
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {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.
|
|
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/
|
{sarj_python_lint-1.27.4 → sarj_python_lint-1.28.0}/src/sarj_python_lint/interpreter_argv.py
RENAMED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
431
|
+
validated_shell: bool = False,
|
|
399
432
|
) -> ParsedOption:
|
|
400
|
-
if
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
)
|