sarj-python-lint 0.87.1__tar.gz → 0.87.3__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 (142) hide show
  1. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/PKG-INFO +1 -1
  2. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/pyproject.toml +1 -1
  3. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rule_base.py +3 -0
  4. sarj_python_lint-0.87.3/src/sarj_python_lint/rules/_ast_position.py +21 -0
  5. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +1 -0
  6. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/fakes_in_shared_location.py +1 -0
  7. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +13 -6
  8. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/iac_source_coupled_test.py +1 -0
  9. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/mock_without_spec.py +1 -0
  10. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/named_record_at_boundaries.py +2 -0
  11. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +1 -0
  12. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +1 -0
  13. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_any_mapping_types.py +2 -0
  14. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +3 -2
  15. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +15 -7
  16. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +2 -0
  17. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +1 -0
  18. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_redundant_literal_description.py +1 -0
  19. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +2 -0
  20. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_restated_comment.py +1 -0
  21. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_typed_doc_sections.py +1 -0
  22. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_unused_value_marker.py +1 -0
  23. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +1 -0
  24. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +1 -0
  25. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/over_mocked_test.py +1 -0
  26. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +2 -0
  27. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +1 -0
  28. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +1 -0
  29. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +1 -0
  30. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +4 -2
  31. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_library_fake.py +1 -0
  32. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +15 -7
  33. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +14 -6
  34. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_module_level_constant.py +1 -0
  35. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +4 -2
  36. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +16 -8
  37. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +1 -0
  38. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_or_pattern.py +1 -0
  39. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +2 -0
  40. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +12 -4
  41. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +2 -0
  42. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_str_enum.py +12 -5
  43. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +1 -0
  44. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +2 -0
  45. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +2 -0
  46. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +2 -0
  47. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +1 -0
  48. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/redundant_docstring.py +1 -0
  49. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +1 -0
  50. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/repeated_static_call_cases.py +1 -0
  51. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +1 -0
  52. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/require_port_for_service.py +27 -20
  53. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +1 -0
  54. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +82 -44
  55. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +32 -16
  56. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +1 -0
  57. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +16 -7
  58. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/typed_error_reasons.py +1 -0
  59. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/unused_test_factory_option.py +1 -0
  60. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/.gitignore +0 -0
  61. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/LICENSE +0 -0
  62. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/README.md +0 -0
  63. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/__init__.py +0 -0
  64. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/__main__.py +0 -0
  65. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/_analysis_session.py +0 -0
  66. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/_filesystem.py +0 -0
  67. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/_python_target.py +0 -0
  68. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  69. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/_secret_names.py +0 -0
  70. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/_version.py +0 -0
  71. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/py.typed +0 -0
  72. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/ratchet.py +0 -0
  73. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/__init__.py +0 -0
  74. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  75. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  76. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_comments.py +0 -0
  77. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  78. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  79. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_first_party.py +0 -0
  80. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  81. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_imports.py +0 -0
  82. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_logging.py +0 -0
  83. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_paths.py +0 -0
  84. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_project_index.py +0 -0
  85. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  86. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_pytest.py +0 -0
  87. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_registry.py +0 -0
  88. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_sql.py +0 -0
  89. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  90. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  91. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  92. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  93. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  94. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  95. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  96. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  97. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  98. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  99. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  100. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  101. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  102. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  103. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  104. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  105. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  106. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  107. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  108. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  109. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  110. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  111. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  112. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  113. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  114. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  115. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  116. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  117. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  118. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  119. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  120. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  121. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  122. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  123. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  124. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  125. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  126. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  127. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  128. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  129. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  130. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  131. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  132. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  133. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  134. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  135. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  136. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  137. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/require_typed_http_test_response.py +0 -0
  138. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  139. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/stepdown.py +0 -0
  140. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  141. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  142. {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/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: 0.87.1
3
+ Version: 0.87.3
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 = "0.87.1"
3
+ version = "0.87.3"
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" }]
@@ -155,6 +155,7 @@ class RuleDocumentation:
155
155
  rationale: str
156
156
  remediation: str
157
157
  category: RuleCategory
158
+ default_level: Severity = Severity.ERROR
158
159
  autofix: AutofixPolicy = AutofixPolicy.NONE
159
160
  aliases: tuple[str, ...] = ()
160
161
  limitations: tuple[str, ...] = ()
@@ -207,6 +208,7 @@ class NativeRuleSpec:
207
208
  rationale: str
208
209
  remediation: str
209
210
  category: RuleCategory
211
+ default_level: Severity
210
212
  autofix: AutofixPolicy
211
213
  aliases: tuple[str, ...]
212
214
  limitations: tuple[str, ...]
@@ -283,6 +285,7 @@ class Rule(ABC):
283
285
  rationale=authored.rationale,
284
286
  remediation=authored.remediation,
285
287
  category=authored.category,
288
+ default_level=authored.default_level,
286
289
  autofix=authored.autofix,
287
290
  aliases=authored.aliases,
288
291
  limitations=authored.limitations,
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import TYPE_CHECKING
5
+
6
+
7
+ if TYPE_CHECKING:
8
+ import ast
9
+
10
+
11
+ @dataclass(frozen=True, order=True, slots=True)
12
+ class AstPosition:
13
+ line: int
14
+ column: int
15
+
16
+
17
+ def ast_position(node: ast.AST, *, missing: int) -> AstPosition:
18
+ return AstPosition(
19
+ line=getattr(node, "lineno", missing),
20
+ column=getattr(node, "col_offset", missing),
21
+ )
@@ -264,6 +264,7 @@ class ComplexPostgresQueryRequiresArchitectureReview(Rule):
264
264
  id: str = "complex-postgres-query-requires-architecture-review"
265
265
  code: str = "SARJ437"
266
266
  documentation = RuleDocumentation(
267
+ default_level=Severity.WARNING,
267
268
  summary="Complex executable PostgreSQL query shapes require architecture review.",
268
269
  rationale=(
269
270
  "Join-heavy, deeply staged, or wide joined reads can obscure cardinality, bounds, ordering, and locking "
@@ -50,6 +50,7 @@ class FakesInSharedLocation(Rule):
50
50
  id = "fakes-in-shared-location"
51
51
  code = "SARJ428"
52
52
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
53
+ default_level=Severity.WARNING,
53
54
  summary="Review named top-level test doubles for shared-support ownership unless they are intentionally scenario-local.",
54
55
  rationale=(
55
56
  "Reusable doubles hidden in an individual test module are difficult to discover and are often recreated. "
@@ -73,6 +73,7 @@ class FastapiExplicitOpenapiContract(Rule):
73
73
  id: str = "fastapi-explicit-openapi-contract"
74
74
  code: str = "SARJ094"
75
75
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
76
+ default_level=Severity.WARNING,
76
77
  summary="Visible FastAPI operations pin locally reviewable metadata and avoid statically provable OpenAPI gaps.",
77
78
  rationale=(
78
79
  "FastAPI can infer valid schemas and default statuses. This stricter organizational policy pins locally "
@@ -674,12 +675,12 @@ def _check_direct_responses(
674
675
  routes: tuple[Route, ...],
675
676
  index: FastapiIndex,
676
677
  ) -> list[_Finding]:
677
- statuses, invalid = _direct_response_statuses(function, index)
678
+ direct = _direct_response_statuses(function, index)
678
679
  findings = [
679
680
  _Finding(node, f"[status] {status} is outside the HTTP status range 100..599.")
680
- for status, node in invalid.items()
681
+ for status, node in direct.invalid.items()
681
682
  ]
682
- if not statuses:
683
+ if not direct.statuses:
683
684
  return findings
684
685
  for route in _operations(routes):
685
686
  responses = _route_response_nodes(route)
@@ -689,7 +690,7 @@ def _check_direct_responses(
689
690
  status for node in responses if isinstance(node, ast.Dict) for status in _response_codes(node, index)
690
691
  }
691
692
  primary = _status_code(route.keywords.get("status_code"), index)
692
- missing = sorted(statuses - documented - ({primary} if primary is not None else set()))
693
+ missing = sorted(direct.statuses - documented - ({primary} if primary is not None else set()))
693
694
  if missing:
694
695
  findings.append(
695
696
  _Finding(
@@ -700,9 +701,15 @@ def _check_direct_responses(
700
701
  return findings
701
702
 
702
703
 
704
+ @dataclass(frozen=True, slots=True)
705
+ class _DirectResponseStatuses:
706
+ statuses: set[int]
707
+ invalid: dict[int, ast.Call]
708
+
709
+
703
710
  def _direct_response_statuses(
704
711
  function: ast.FunctionDef | ast.AsyncFunctionDef, index: FastapiIndex
705
- ) -> tuple[set[int], dict[int, ast.Call]]:
712
+ ) -> _DirectResponseStatuses:
706
713
  statuses: set[int] = set()
707
714
  invalid: dict[int, ast.Call] = {}
708
715
  stack: list[tuple[ast.AST, bool]] = [(node, False) for node in function.body]
@@ -721,7 +728,7 @@ def _direct_response_statuses(
721
728
  else:
722
729
  invalid.setdefault(status, call)
723
730
  stack.extend((child, catches_http_exception) for child in children(current))
724
- return statuses, invalid
731
+ return _DirectResponseStatuses(statuses=statuses, invalid=invalid)
725
732
 
726
733
 
727
734
  def _extend_response_try(
@@ -32,6 +32,7 @@ class IacSourceCoupledTest(Rule):
32
32
  id = "iac-source-coupled-test"
33
33
  code = "SARJ412"
34
34
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
35
+ default_level=Severity.WARNING,
35
36
  summary="Test uses raw Terraform/HCL text as an infrastructure-behavior oracle.",
36
37
  rationale=(
37
38
  "Substring and regex checks can pass on comments, formatting, or unreachable Terraform configuration "
@@ -121,6 +121,7 @@ class MockWithoutSpec(Rule):
121
121
  id: str = "mock-without-spec"
122
122
  code: str = "SARJ040"
123
123
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
124
+ default_level=Severity.WARNING,
124
125
  summary="Unrestricted mock permits attributes outside the collaborator contract.",
125
126
  rationale=(
126
127
  "A mock without a spec permits removed or misspelled attributes; a callable mock without autospec may "
@@ -13,6 +13,7 @@ from sarj_python_lint.rule_base import (
13
13
  RuleCategory,
14
14
  RuleDocumentation,
15
15
  RuleExample,
16
+ Severity,
16
17
  is_suppressed,
17
18
  parse_or_none,
18
19
  )
@@ -42,6 +43,7 @@ class NamedRecordAtBoundaries(Rule):
42
43
  id: str = "named-record-at-boundaries"
43
44
  code: str = "SARJ008"
44
45
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
46
+ default_level=Severity.WARNING,
45
47
  summary="Public Python API returns an unnamed fixed-shape record.",
46
48
  rationale="A named record makes field types and required keys explicit to callers and static tooling.",
47
49
  remediation="Define and return a `TypedDict`, Pydantic model, or frozen dataclass for the fixed record shape.",
@@ -204,6 +204,7 @@ class NegativeOnlyHttpStatusAssertion(Rule):
204
204
  id = "negative-only-http-status-assertion"
205
205
  code = "SARJ408"
206
206
  documentation = RuleDocumentation(
207
+ default_level=Severity.WARNING,
207
208
  summary="HTTP test assertion only excludes a server error instead of identifying the intended response.",
208
209
  rationale=(
209
210
  "Authentication, routing, validation, and domain failures can all replace the intended response while "
@@ -152,6 +152,7 @@ class NoAnalyticalAggregationInPostgresStore(Rule):
152
152
  id: str = "no-analytical-aggregation-in-postgres-store"
153
153
  code: str = "SARJ020"
154
154
  documentation = RuleDocumentation(
155
+ default_level=Severity.WARNING,
155
156
  summary="Potentially analytical PostgreSQL store queries require review.",
156
157
  rationale=(
157
158
  "Unbounded reporting and statistical scans can compete with transactional reads. "
@@ -13,6 +13,7 @@ from sarj_python_lint.rule_base import (
13
13
  RuleCategory,
14
14
  RuleDocumentation,
15
15
  RuleExample,
16
+ Severity,
16
17
  is_suppressed,
17
18
  )
18
19
  from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics, scope_bound_names
@@ -37,6 +38,7 @@ class NoAnyMappingTypes(Rule):
37
38
  id = "no-any-mapping-types"
38
39
  code = "SARJ447"
39
40
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
41
+ default_level=Severity.WARNING,
40
42
  summary="String-keyed mapping types must not erase their values with `Any`.",
41
43
  rationale=(
42
44
  "An `Any`-valued mapping disables checking transitively and conceals whether the value is a fixed record, "
@@ -20,6 +20,7 @@ from sarj_python_lint.rule_base import (
20
20
  is_suppressed,
21
21
  parse_or_none,
22
22
  )
23
+ from sarj_python_lint.rules._ast_position import AstPosition, ast_position
23
24
  from sarj_python_lint.rules._imports import ImportIndex
24
25
  from sarj_python_lint.rules._paths import is_generated, is_test_path, is_test_support_path
25
26
 
@@ -954,8 +955,8 @@ def _target_names(node: ast.expr) -> set[str]:
954
955
  return set()
955
956
 
956
957
 
957
- def _position(node: ast.AST) -> tuple[int, int]:
958
- return getattr(node, "lineno", -1), getattr(node, "col_offset", -1)
958
+ def _position(node: ast.AST) -> AstPosition:
959
+ return ast_position(node, missing=-1)
959
960
 
960
961
 
961
962
  def _scope_nodes(statements: Iterable[ast.stmt], *, stop_rebinding: str | None = None) -> Iterator[ast.AST]:
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import ast
4
+ from dataclasses import dataclass
4
5
  from pathlib import PurePosixPath
5
6
  import re
6
7
  from typing import TYPE_CHECKING, ClassVar, final, override
@@ -26,6 +27,13 @@ if TYPE_CHECKING:
26
27
 
27
28
  _RANDOM_UUID_RE = re.compile(r"\b(?:gen_random_uuid|uuid_generate_v4)\s*\(", re.IGNORECASE)
28
29
 
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class _IdentifierEnd:
33
+ end: int
34
+ after_whitespace: int
35
+
36
+
29
37
  # Require SQL structure so prose that merely names the function stays valid.
30
38
  _SQL_SHAPE_RE = re.compile(
31
39
  r"\b(?:CREATE|ALTER|INSERT|UPDATE|SELECT|DEFAULT|VALUES)\b",
@@ -132,12 +140,12 @@ def _inside_uuidv7_builder(sql: str, position: int) -> bool:
132
140
  index = 0
133
141
  while index < position:
134
142
  if sql[index].isalpha() or sql[index] == "_":
135
- end, after = _identifier_end(sql, index, position)
136
- if after < position and sql[after] == "(":
137
- calls.append(sql[index:end].lower())
138
- index = after + 1
143
+ identifier = _identifier_end(sql, index, position)
144
+ if identifier.after_whitespace < position and sql[identifier.after_whitespace] == "(":
145
+ calls.append(sql[index : identifier.end].lower())
146
+ index = identifier.after_whitespace + 1
139
147
  continue
140
- index = end
148
+ index = identifier.end
141
149
  continue
142
150
  if sql[index] == "(":
143
151
  calls.append(None)
@@ -147,11 +155,11 @@ def _inside_uuidv7_builder(sql: str, position: int) -> bool:
147
155
  return any(call in _UUIDV7_BUILDERS for call in calls)
148
156
 
149
157
 
150
- def _identifier_end(sql: str, index: int, position: int) -> tuple[int, int]:
158
+ def _identifier_end(sql: str, index: int, position: int) -> _IdentifierEnd:
151
159
  end = index + 1
152
160
  while end < position and (sql[end].isalnum() or sql[end] == "_"):
153
161
  end += 1
154
162
  after = end
155
163
  while after < position and sql[after].isspace():
156
164
  after += 1
157
- return end, after
165
+ return _IdentifierEnd(end=end, after_whitespace=after)
@@ -13,6 +13,7 @@ from sarj_python_lint.rule_base import (
13
13
  RuleCategory,
14
14
  RuleDocumentation,
15
15
  RuleExample,
16
+ Severity,
16
17
  parse_or_none,
17
18
  )
18
19
  from sarj_python_lint.rules._paths import is_generated, is_test_path, is_test_support_path
@@ -30,6 +31,7 @@ class NoRawConnectionInTests(Rule):
30
31
  id = "no-raw-connection-in-tests"
31
32
  code = "SARJ429"
32
33
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
34
+ default_level=Severity.WARNING,
33
35
  summary="Do not acquire raw database connections in tests.",
34
36
  rationale=(
35
37
  "Tests that reach through a pool couple assertions and setup to persistence internals, bypass the "
@@ -112,6 +112,7 @@ class NoRawSourceTextTestOracle(Rule):
112
112
  id = "no-raw-source-text-test-oracle"
113
113
  code = "SARJ402"
114
114
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
115
+ default_level=Severity.WARNING,
115
116
  summary="Test uses raw text from a source-like project path as its oracle.",
116
117
  rationale="Substring and regex checks can pass on comments or unreachable configuration and fail after behavior-preserving formatting changes.",
117
118
  remediation=(
@@ -44,6 +44,7 @@ class NoRedundantLiteralDescription(Rule):
44
44
  id = "no-restated-closed-domain-description"
45
45
  code = "SARJ423"
46
46
  documentation = RuleDocumentation(
47
+ default_level=Severity.WARNING,
47
48
  summary="Do not restate a string Literal or local string Enum domain in its Pydantic description.",
48
49
  rationale=(
49
50
  "Pydantic already publishes literal and enum domains in JSON Schema; duplicate must-be prose can "
@@ -14,6 +14,7 @@ from sarj_python_lint.rule_base import (
14
14
  RuleCategory,
15
15
  RuleDocumentation,
16
16
  RuleExample,
17
+ Severity,
17
18
  parse_or_none,
18
19
  )
19
20
  from sarj_python_lint.rules._imports import ImportIndex
@@ -33,6 +34,7 @@ class NoRedundantModuleAliasExports(Rule):
33
34
  id: str = "no-redundant-module-alias-exports"
34
35
  code: str = "SARJ440"
35
36
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
37
+ default_level=Severity.WARNING,
36
38
  summary="Do not manufacture public APIs from private names or replace the current module.",
37
39
  rationale=(
38
40
  "Binding a public name to a private implementation creates two names for one API and disguises the "
@@ -253,6 +253,7 @@ class NoRestatedComment(Rule):
253
253
  id: str = "no-restated-comment"
254
254
  code: str = "SARJ049"
255
255
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
256
+ default_level=Severity.WARNING,
256
257
  summary="Short standalone comment lexically restates the immediately following simple action.",
257
258
  rationale=(
258
259
  "When a comment adds no rationale, scope, ordering, or constraint, it duplicates code and can become stale. "
@@ -28,6 +28,7 @@ class NoTypedDocSections(Rule):
28
28
  id = "no-docstring-type-restatement"
29
29
  code = "SARJ092"
30
30
  documentation = RuleDocumentation(
31
+ default_level=Severity.WARNING,
31
32
  summary="A docstring type label repeats an annotation from the fully typed signature.",
32
33
  rationale=(
33
34
  "A repeated type spelling can drift from the annotation and obscures the behavioral contract that only prose "
@@ -29,6 +29,7 @@ class NoUnusedValueMarker(Rule):
29
29
  id = "no-unused-value-marker"
30
30
  code = "SARJ452"
31
31
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
32
+ default_level=Severity.WARNING,
32
33
  summary="Do not use standalone assignments to `_` to discard values.",
33
34
  rationale=(
34
35
  "A standalone `_ = value` assignment adds a binding without clarifying intent. A side-effecting call can "
@@ -114,6 +114,7 @@ class NoWholeRequestResponsePayloadInLog(Rule):
114
114
  id: str = "no-whole-request-response-payload-in-log"
115
115
  code: str = "SARJ436"
116
116
  documentation = RuleDocumentation(
117
+ default_level=Severity.WARNING,
117
118
  summary="Whole request or response payloads passed to logging calls require review.",
118
119
  rationale=(
119
120
  "Request and response bodies often contain personal, financial, or echoed authentication data, while "
@@ -56,6 +56,7 @@ class OpaqueParametrizeCaseNeedsId(Rule):
56
56
  id: str = "opaque-parametrize-case-needs-id"
57
57
  code: str = "SARJ042"
58
58
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
59
+ default_level=Severity.WARNING,
59
60
  summary="Opaque static pytest parameter cases rely on argument-name-and-index fallback IDs.",
60
61
  rationale="Fallback IDs such as `payload0` are hard to diagnose and silently change when the table is reordered.",
61
62
  remediation=(
@@ -179,6 +179,7 @@ class OverMockedTest(Rule):
179
179
  id: str = "over-mocked-test"
180
180
  code: str = "SARJ062"
181
181
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
182
+ default_level=Severity.WARNING,
182
183
  summary="Tests should not use more than five independently rooted test doubles or collaborator substitutions.",
183
184
  rationale="Broad double setup obscures the behavior under test and often couples tests to implementation wiring.",
184
185
  remediation=(
@@ -18,6 +18,7 @@ from sarj_python_lint.rule_base import (
18
18
  RuleCategory,
19
19
  RuleDocumentation,
20
20
  RuleExample,
21
+ Severity,
21
22
  is_suppressed,
22
23
  parse_or_none,
23
24
  )
@@ -77,6 +78,7 @@ class PreferCollectionComprehension(Rule):
77
78
  id = "prefer-collection-comprehension"
78
79
  code = "SARJ430"
79
80
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
81
+ default_level=Severity.WARNING,
80
82
  summary="Single-purpose fresh collection builder loop — prefer a direct comprehension.",
81
83
  rationale=(
82
84
  "An empty collection followed by a loop whose only behavior is one projection or filtered insertion "
@@ -94,6 +94,7 @@ class PreferConstantTimeSecretCompare(Rule):
94
94
  id: str = "prefer-constant-time-secret-compare"
95
95
  code: str = "SARJ011"
96
96
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
97
+ default_level=Severity.WARNING,
97
98
  summary="Externally supplied authenticators are compared with timing-sensitive equality.",
98
99
  rationale=(
99
100
  "Ordinary equality may short-circuit based on matching content when attacker-controlled credentials are "
@@ -120,6 +120,7 @@ class PreferFstringOverConcat(Rule):
120
120
  id: str = "prefer-fstring-over-concat"
121
121
  code: str = "SARJ068"
122
122
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
123
+ default_level=Severity.WARNING,
123
124
  summary="Prefer f-strings for short human-readable interpolation when they make the result clearer.",
124
125
  rationale="For prose-like strings, f-strings keep interpolated values and surrounding text visible together.",
125
126
  remediation=(
@@ -73,6 +73,7 @@ class PreferImmutableModuleConstant(Rule):
73
73
  id: str = "prefer-immutable-module-constant"
74
74
  code: str = "SARJ096"
75
75
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
76
+ default_level=Severity.WARNING,
76
77
  summary=("Nonempty uppercase module collections allow top-level membership or keys to change at runtime."),
77
78
  rationale=(
78
79
  "A constant-looking collection can expose process-wide top-level mutation even when callers intend it as a "
@@ -16,6 +16,7 @@ from sarj_python_lint.rule_base import (
16
16
  Severity,
17
17
  parse_or_none,
18
18
  )
19
+ from sarj_python_lint.rules._ast_position import AstPosition, ast_position
19
20
  from sarj_python_lint.rules._imports import ImportIndex
20
21
  from sarj_python_lint.rules._paths import is_generated, is_test_path
21
22
 
@@ -36,6 +37,7 @@ class PreferInjectedDependencyOverMonkeypatch(Rule):
36
37
  id: str = "prefer-injected-dependency-over-monkeypatch"
37
38
  code: str = "SARJ445"
38
39
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
40
+ default_level=Severity.WARNING,
39
41
  summary="Tests should inject dependencies instead of replacing attributes through ambient patching.",
40
42
  rationale=(
41
43
  "Attribute patching hides collaborators and configuration behind ambient module or object state, coupling "
@@ -250,10 +252,10 @@ def _parameter_rebound_before(
250
252
  return any(handle in _bound_targets(node) and _position(node) < use_position for node in nodes)
251
253
 
252
254
 
253
- def _position(node: ast.AST) -> tuple[int, int]:
255
+ def _position(node: ast.AST) -> AstPosition:
254
256
  if isinstance(node, ast.withitem):
255
257
  return _position(node.context_expr)
256
- return (getattr(node, "lineno", 0), getattr(node, "col_offset", 0))
258
+ return ast_position(node, missing=0)
257
259
 
258
260
 
259
261
  def _direct_aliases(nodes: tuple[ast.AST, ...], handles: set[str], parameters: set[str]) -> set[str]:
@@ -341,6 +341,7 @@ class PreferLibraryFake(Rule):
341
341
  id: str = "prefer-library-fake"
342
342
  code: str = "SARJ059"
343
343
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
344
+ default_level=Severity.WARNING,
344
345
  summary="Prefer a maintained fake, emulator, recorder, or test service for substantial third-party protocols.",
345
346
  rationale=(
346
347
  "A substantial hand-written protocol model can drift from supported request, state, and error behavior."
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import ast
4
+ from dataclasses import dataclass
4
5
  from pathlib import PurePosixPath
5
6
  from typing import TYPE_CHECKING, ClassVar, NamedTuple, final, override
6
7
 
@@ -85,6 +86,7 @@ class PreferMatchExceptionDispatch(Rule):
85
86
  id: str = "prefer-match-exception-dispatch"
86
87
  code: str = "SARJ453"
87
88
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
89
+ default_level=Severity.WARNING,
88
90
  summary="Prefer guarded match/case for refined exception type dispatch.",
89
91
  rationale=(
90
92
  "A long exception classifier that repeats isinstance checks and then refines one exception type hides "
@@ -215,17 +217,23 @@ def _classifier_starts(
215
217
  for statements in _nested_statement_lists(body):
216
218
  index = 0
217
219
  while index < len(statements):
218
- candidates, cursor = _adjacent_bare_ifs(statements, index)
219
- branches = _validated_branches(candidates, subject, imports)
220
- if branches is not None and _is_classifier(branches, statements, cursor):
220
+ adjacent = _adjacent_bare_ifs(statements, index)
221
+ branches = _validated_branches(adjacent.candidates, subject, imports)
222
+ if branches is not None and _is_classifier(branches, statements, adjacent.cursor):
221
223
  findings.append(branches[0][0])
222
- index = cursor + 1
224
+ index = adjacent.cursor + 1
223
225
  else:
224
- index = max(cursor, index + 1)
226
+ index = max(adjacent.cursor, index + 1)
225
227
  return tuple(findings)
226
228
 
227
229
 
228
- def _adjacent_bare_ifs(statements: list[ast.stmt], start: int) -> tuple[list[ast.If], int]:
230
+ @dataclass(frozen=True, slots=True)
231
+ class _AdjacentBareIfs:
232
+ candidates: list[ast.If]
233
+ cursor: int
234
+
235
+
236
+ def _adjacent_bare_ifs(statements: list[ast.stmt], start: int) -> _AdjacentBareIfs:
229
237
  cursor = start
230
238
  candidates: list[ast.If] = []
231
239
  while cursor < len(statements):
@@ -234,7 +242,7 @@ def _adjacent_bare_ifs(statements: list[ast.stmt], start: int) -> tuple[list[ast
234
242
  break
235
243
  candidates.append(candidate)
236
244
  cursor += 1
237
- return candidates, cursor
245
+ return _AdjacentBareIfs(candidates=candidates, cursor=cursor)
238
246
 
239
247
 
240
248
  def _validated_branches(
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import ast
4
4
  from collections import Counter
5
+ from dataclasses import dataclass
5
6
  from pathlib import PurePosixPath
6
7
  from typing import TYPE_CHECKING, ClassVar, final, override
7
8
 
@@ -37,6 +38,7 @@ class PreferMatchValueDispatch(Rule):
37
38
  id: str = "prefer-match-value-dispatch"
38
39
  code: str = "SARJ439"
39
40
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
41
+ default_level=Severity.WARNING,
40
42
  summary="Prefer match/case for repeated dispatch on one value with a fallback.",
41
43
  rationale="A single dispatch subject makes distinct value cases and their fallback easier to review.",
42
44
  remediation=(
@@ -164,12 +166,12 @@ class PreferMatchValueDispatch(Rule):
164
166
  for node in ast.walk(tree):
165
167
  if not isinstance(node, ast.If) or id(node) in continuations:
166
168
  continue
167
- branches, current = _elif_chain(node, lines, continuations)
168
- if len(branches) < _MIN_TESTS or not current.orelse:
169
+ chain = _elif_chain(node, lines, continuations)
170
+ if len(chain.branches) < _MIN_TESTS or not chain.last.orelse:
169
171
  continue
170
- subject = _dispatch_subject(branches, constants)
172
+ subject = _dispatch_subject(chain.branches, constants)
171
173
  if subject is None:
172
- subject = _expanded_dispatch_subject(branches, constants)
174
+ subject = _expanded_dispatch_subject(chain.branches, constants)
173
175
  if subject is not None:
174
176
  candidates.append((node, subject))
175
177
  for branches in _terminal_sibling_dispatches(tree):
@@ -368,7 +370,13 @@ def _subject(expression: ast.expr) -> str | None:
368
370
  return None
369
371
 
370
372
 
371
- def _elif_chain(node: ast.If, lines: list[str], continuations: set[int]) -> tuple[list[ast.If], ast.If]:
373
+ @dataclass(frozen=True, slots=True)
374
+ class _ElifChain:
375
+ branches: list[ast.If]
376
+ last: ast.If
377
+
378
+
379
+ def _elif_chain(node: ast.If, lines: list[str], continuations: set[int]) -> _ElifChain:
372
380
  branches = [node]
373
381
  current = node
374
382
  while len(current.orelse) == 1:
@@ -378,7 +386,7 @@ def _elif_chain(node: ast.If, lines: list[str], continuations: set[int]) -> tupl
378
386
  continuations.add(id(child))
379
387
  branches.append(child)
380
388
  current = child
381
- return branches, current
389
+ return _ElifChain(branches=branches, last=current)
382
390
 
383
391
 
384
392
  def _collect_terminal_siblings(field: list[ast.stmt], candidates: list[list[ast.If]]) -> None:
@@ -96,6 +96,7 @@ class PreferModuleLevelConstant(Rule):
96
96
  id: str = "prefer-module-level-constant"
97
97
  code: str = "SARJ039"
98
98
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
99
+ default_level=Severity.WARNING,
99
100
  summary="Hoist repeatedly read static values when an immutable module representation preserves behavior.",
100
101
  rationale=(
101
102
  "Rebuilding a substantial static collection repeats allocation, while calling `re.compile` with a "
@@ -16,6 +16,7 @@ from sarj_python_lint.rule_base import (
16
16
  Severity,
17
17
  parse_or_none,
18
18
  )
19
+ from sarj_python_lint.rules._ast_position import AstPosition, ast_position
19
20
  from sarj_python_lint.rules._imports import ImportIndex
20
21
  from sarj_python_lint.rules._paths import is_generated, is_test_path
21
22
 
@@ -43,6 +44,7 @@ class PreferMonkeypatchForProcessStateInTest(Rule):
43
44
  id = "prefer-monkeypatch-for-process-state-in-test"
44
45
  code = "SARJ446"
45
46
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
47
+ default_level=Severity.WARNING,
46
48
  summary="Test mutates process-wide state without a restoring test scope.",
47
49
  rationale=(
48
50
  "The working directory and Python import registries are shared by every test in the process. A direct "
@@ -294,8 +296,8 @@ def _import_root_rebound_before(
294
296
  )
295
297
 
296
298
 
297
- def _position(node: ast.AST) -> tuple[int, int]:
298
- return (getattr(node, "lineno", 0), getattr(node, "col_offset", 0))
299
+ def _position(node: ast.AST) -> AstPosition:
300
+ return ast_position(node, missing=0)
299
301
 
300
302
 
301
303
  def _restored_mutations(