sarj-python-lint 0.78.0__tar.gz → 0.78.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/PKG-INFO +1 -1
  2. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/pyproject.toml +1 -1
  3. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_registry.py +31 -27
  4. sarj_python_lint-0.78.0/src/sarj_python_lint/rules/defect_xfail_requires_strict.py → sarj_python_lint-0.78.2/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +34 -8
  5. sarj_python_lint-0.78.2/src/sarj_python_lint/rules/fakes_in_shared_location.py +113 -0
  6. sarj_python_lint-0.78.0/src/sarj_python_lint/rules/fastapi_openapi_contract.py → sarj_python_lint-0.78.2/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +50 -13
  7. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/iac_source_coupled_test.py +3 -2
  8. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +196 -23
  9. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/mock_without_spec.py +13 -5
  10. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +69 -5
  11. sarj_python_lint-0.78.0/src/sarj_python_lint/rules/no_aggregation_in_store_query.py → sarj_python_lint-0.78.2/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +21 -5
  12. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_comment_cruft.py +6 -6
  13. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +73 -12
  14. sarj_python_lint-0.78.0/src/sarj_python_lint/rules/duplicated_override_docstring.py → sarj_python_lint-0.78.2/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +104 -30
  15. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +58 -3
  16. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_fastapi_on_event.py +54 -24
  17. sarj_python_lint-0.78.0/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py → sarj_python_lint-0.78.2/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +26 -14
  18. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_first_party_private_import.py +91 -11
  19. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +40 -7
  20. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_generic_single_export_module.py +7 -5
  21. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +24 -5
  22. sarj_python_lint-0.78.0/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py → sarj_python_lint-0.78.2/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +43 -12
  23. sarj_python_lint-0.78.2/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +154 -0
  24. sarj_python_lint-0.78.0/src/sarj_python_lint/rules/duplicate_test_body.py → sarj_python_lint-0.78.2/src/sarj_python_lint/rules/no_repeated_test_body.py +6 -5
  25. sarj_python_lint-0.78.2/src/sarj_python_lint/rules/prefer_collection_comprehension.py +441 -0
  26. sarj_python_lint-0.78.0/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py → sarj_python_lint-0.78.2/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +37 -13
  27. sarj_python_lint-0.78.0/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py → sarj_python_lint-0.78.2/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +21 -26
  28. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/repeated_static_call_cases.py +2 -2
  29. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/source_coupled_test.py +1 -1
  30. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/stepdown.py +1 -4
  31. sarj_python_lint-0.78.0/src/sarj_python_lint/rules/get_delegates_to_get_many.py → sarj_python_lint-0.78.2/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +78 -29
  32. sarj_python_lint-0.78.0/src/sarj_python_lint/rules/created_at_order_requires_tiebreaker.py → sarj_python_lint-0.78.2/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +17 -16
  33. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/.gitignore +0 -0
  34. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/LICENSE +0 -0
  35. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/README.md +0 -0
  36. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/__init__.py +0 -0
  37. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/__main__.py +0 -0
  38. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/_analysis_session.py +0 -0
  39. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/_filesystem.py +0 -0
  40. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  41. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/_secret_names.py +0 -0
  42. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/_version.py +0 -0
  43. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/py.typed +0 -0
  44. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/ratchet.py +0 -0
  45. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rule_base.py +0 -0
  46. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/__init__.py +0 -0
  47. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  48. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_comments.py +0 -0
  49. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  50. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  51. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_first_party.py +0 -0
  52. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_imports.py +0 -0
  53. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_logging.py +0 -0
  54. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_paths.py +0 -0
  55. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_project_index.py +0 -0
  56. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  57. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_pytest.py +0 -0
  58. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_sql.py +0 -0
  59. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  60. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  61. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  62. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  63. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  64. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
  65. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_long_comment.py +0 -0
  66. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  67. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  68. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
  69. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
  70. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  71. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  72. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  73. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  74. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  75. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
  76. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
  77. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  78. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  79. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  80. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  81. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  82. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  83. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  84. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  85. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  86. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  87. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  88. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  89. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  90. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  91. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  92. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  93. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  94. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  95. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  96. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  97. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_one_for_required_row.py +0 -0
  98. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  99. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  100. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  101. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  102. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  103. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
  104. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  105. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  106. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  107. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/preserve_declared_nominal_id.py +0 -0
  108. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/preserve_enum_types.py +0 -0
  109. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  110. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
  111. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  112. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  113. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  114. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  115. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  116. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  117. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  118. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  119. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/require_validated_row_factory.py +0 -0
  120. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  121. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/sql_requires_injected_pool_owner.py +0 -0
  122. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  123. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  124. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
  125. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  126. {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/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.78.0
3
+ Version: 0.78.2
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.78.0"
3
+ version = "0.78.2"
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" }]
@@ -3,50 +3,43 @@ from __future__ import annotations
3
3
  from types import MappingProxyType
4
4
  from typing import TYPE_CHECKING
5
5
 
6
- from sarj_python_lint.rules.created_at_order_requires_tiebreaker import (
7
- CreatedAtOrderRequiresTiebreaker,
8
- )
9
- from sarj_python_lint.rules.defect_xfail_requires_strict import DefectXfailRequiresStrict
6
+ from sarj_python_lint.rules.defect_xfail_requires_explicit_strict import DefectXfailRequiresExplicitStrict
10
7
  from sarj_python_lint.rules.docstring_args_restate_signature import (
11
8
  DocstringArgsRestateSignature,
12
9
  )
13
10
  from sarj_python_lint.rules.docstring_returns_restate_signature import (
14
11
  DocstringReturnsRestateSignature,
15
12
  )
16
- from sarj_python_lint.rules.duplicate_test_body import DuplicateTestBody
17
- from sarj_python_lint.rules.duplicated_override_docstring import (
18
- DuplicatedOverrideDocstring,
19
- )
20
- from sarj_python_lint.rules.fastapi_openapi_contract import FastapiOpenapiContract
21
- from sarj_python_lint.rules.fixture_returns_bare_tuple import FixtureReturnsBareTuple
22
- from sarj_python_lint.rules.get_delegates_to_get_many import GetDelegatesToGetMany
13
+ from sarj_python_lint.rules.fakes_in_shared_location import FakesInSharedLocation
14
+ from sarj_python_lint.rules.fastapi_explicit_openapi_contract import FastapiExplicitOpenapiContract
23
15
  from sarj_python_lint.rules.iac_source_coupled_test import IacSourceCoupledTest
24
16
  from sarj_python_lint.rules.invalid_pydantic_field_default import (
25
17
  InvalidPydanticFieldDefault,
26
18
  )
27
- from sarj_python_lint.rules.kwarg_heavy_construction_in_test import KwargHeavyConstructionInTest
28
19
  from sarj_python_lint.rules.mock_without_spec import MockWithoutSpec
29
20
  from sarj_python_lint.rules.negative_only_http_status_assertion import (
30
21
  NegativeOnlyHttpStatusAssertion,
31
22
  )
32
- from sarj_python_lint.rules.no_aggregation_in_store_query import (
33
- NoAggregationInStoreQuery,
23
+ from sarj_python_lint.rules.no_analytical_aggregation_in_postgres_store import (
24
+ NoAnalyticalAggregationInPostgresStore,
34
25
  )
35
26
  from sarj_python_lint.rules.no_comment_cruft import NoCommentCruft
36
27
  from sarj_python_lint.rules.no_conftest_test_module_import import NoConftestTestModuleImport
28
+ from sarj_python_lint.rules.no_copied_inherited_docstring import NoCopiedInheritedDocstring
37
29
  from sarj_python_lint.rules.no_cors_wildcard_with_credentials import (
38
30
  NoCorsWildcardWithCredentials,
39
31
  )
40
32
  from sarj_python_lint.rules.no_duplicate_dunder_all_entry import NoDuplicateDunderAllEntry
41
33
  from sarj_python_lint.rules.no_fastapi_on_event import NoFastapiOnEvent
42
- from sarj_python_lint.rules.no_file_level_escape_hatch_noqa import NoFileLevelEscapeHatchNoqa
34
+ from sarj_python_lint.rules.no_file_level_escape_hatch_suppression import (
35
+ NoFileLevelEscapeHatchSuppression,
36
+ )
43
37
  from sarj_python_lint.rules.no_first_party_private_import import (
44
38
  NoFirstPartyPrivateImport,
45
39
  )
46
40
  from sarj_python_lint.rules.no_frozen_after_validator_field_write import (
47
41
  NoFrozenAfterValidatorFieldWrite,
48
42
  )
49
- from sarj_python_lint.rules.no_gen_random_uuid_in_sql import NoGenRandomUuidInSql
50
43
  from sarj_python_lint.rules.no_generic_single_export_module import NoGenericSingleExportModule
51
44
  from sarj_python_lint.rules.no_hidden_constructor_fallback import (
52
45
  NoHiddenConstructorFallback,
@@ -59,8 +52,11 @@ from sarj_python_lint.rules.no_optional_tenant_predicate import (
59
52
  NoOptionalTenantPredicate,
60
53
  )
61
54
  from sarj_python_lint.rules.no_query_with_many_joins import NoQueryWithManyJoins
55
+ from sarj_python_lint.rules.no_random_uuid_in_sql import NoRandomUuidInSql
56
+ from sarj_python_lint.rules.no_raw_connection_in_tests import NoRawConnectionInTests
62
57
  from sarj_python_lint.rules.no_redundant_literal_description import NoRedundantLiteralDescription
63
58
  from sarj_python_lint.rules.no_repeated_string_literal import NoRepeatedStringLiteral
59
+ from sarj_python_lint.rules.no_repeated_test_body import NoRepeatedTestBody
64
60
  from sarj_python_lint.rules.no_restated_comment import NoRestatedComment
65
61
  from sarj_python_lint.rules.no_secret_in_log import NoSecretInLog
66
62
  from sarj_python_lint.rules.no_select_star import NoSelectStar
@@ -80,6 +76,7 @@ from sarj_python_lint.rules.opaque_parametrize_case_needs_id import OpaqueParame
80
76
  from sarj_python_lint.rules.over_mocked_test import OverMockedTest
81
77
  from sarj_python_lint.rules.phase_label_comment import TestPhaseLabelComment
82
78
  from sarj_python_lint.rules.prefer_class_row import PreferClassRow
79
+ from sarj_python_lint.rules.prefer_collection_comprehension import PreferCollectionComprehension
83
80
  from sarj_python_lint.rules.prefer_constant_time_secret_compare import (
84
81
  PreferConstantTimeSecretCompare,
85
82
  )
@@ -122,9 +119,11 @@ from sarj_python_lint.rules.preserve_declared_nominal_id import PreserveDeclared
122
119
  from sarj_python_lint.rules.preserve_enum_types import PreserveEnumTypes
123
120
  from sarj_python_lint.rules.production_derived_test_cases import ProductionDerivedTestCases
124
121
  from sarj_python_lint.rules.pydantic_at_boundaries import PydanticAtBoundaries
122
+ from sarj_python_lint.rules.pytest_fixture_returns_bare_tuple import PytestFixtureReturnsBareTuple
125
123
  from sarj_python_lint.rules.redundant_class_docstring import RedundantClassDocstring
126
124
  from sarj_python_lint.rules.redundant_docstring import RedundantDocstring
127
125
  from sarj_python_lint.rules.redundant_module_docstring import RedundantModuleDocstring
126
+ from sarj_python_lint.rules.repeated_kwarg_heavy_call_in_test import RepeatedKwargHeavyCallInTest
128
127
  from sarj_python_lint.rules.repeated_static_call_cases import RepeatedStaticCallCases
129
128
  from sarj_python_lint.rules.require_keyword_only_swap_prone_params import (
130
129
  RequireKeywordOnlySwapProneParams,
@@ -144,9 +143,11 @@ from sarj_python_lint.rules.restated_test_docstring import RestatedTestDocstring
144
143
  from sarj_python_lint.rules.source_coupled_test import SourceCoupledTest
145
144
  from sarj_python_lint.rules.sql_requires_injected_pool_owner import SqlRequiresInjectedPoolOwner
146
145
  from sarj_python_lint.rules.stepdown import Stepdown
146
+ from sarj_python_lint.rules.store_get_delegates_to_bulk_read import StoreGetDelegatesToBulkRead
147
147
  from sarj_python_lint.rules.store_insert_requires_on_conflict import (
148
148
  StoreInsertRequiresOnConflict,
149
149
  )
150
+ from sarj_python_lint.rules.timestamp_order_requires_tiebreaker import TimestampOrderRequiresTiebreaker
150
151
  from sarj_python_lint.rules.trailing_value_narration import TrailingValueNarration
151
152
  from sarj_python_lint.rules.trivially_true_assertion import TriviallyTrueAssertion
152
153
  from sarj_python_lint.rules.uncontrolled_randomness_in_test import UncontrolledRandomnessInTest
@@ -160,16 +161,18 @@ if TYPE_CHECKING:
160
161
 
161
162
  REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
162
163
  {
163
- CreatedAtOrderRequiresTiebreaker.id: CreatedAtOrderRequiresTiebreaker,
164
+ TimestampOrderRequiresTiebreaker.id: TimestampOrderRequiresTiebreaker,
164
165
  NoStringConcatInLoop.id: NoStringConcatInLoop,
165
166
  PreferClassRow.id: PreferClassRow,
167
+ PreferCollectionComprehension.id: PreferCollectionComprehension,
166
168
  PreferStrEnum.id: PreferStrEnum,
167
169
  NoIsinstanceUnionChain.id: NoIsinstanceUnionChain,
168
170
  NoOffsetPagination.id: NoOffsetPagination,
169
171
  PreferNamedtupleOverTupleReturn.id: PreferNamedtupleOverTupleReturn,
170
172
  NoCorsWildcardWithCredentials.id: NoCorsWildcardWithCredentials,
171
173
  PydanticAtBoundaries.id: PydanticAtBoundaries,
172
- FastapiOpenapiContract.id: FastapiOpenapiContract,
174
+ FastapiExplicitOpenapiContract.id: FastapiExplicitOpenapiContract,
175
+ FakesInSharedLocation.id: FakesInSharedLocation,
173
176
  NoSentinelReturnOnExcept.id: NoSentinelReturnOnExcept,
174
177
  PreferConstantTimeSecretCompare.id: PreferConstantTimeSecretCompare,
175
178
  NoSecretInLog.id: NoSecretInLog,
@@ -181,7 +184,8 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
181
184
  SourceCoupledTest.id: SourceCoupledTest,
182
185
  IacSourceCoupledTest.id: IacSourceCoupledTest,
183
186
  NoQueryWithManyJoins.id: NoQueryWithManyJoins,
184
- NoAggregationInStoreQuery.id: NoAggregationInStoreQuery,
187
+ NoRawConnectionInTests.id: NoRawConnectionInTests,
188
+ NoAnalyticalAggregationInPostgresStore.id: NoAnalyticalAggregationInPostgresStore,
185
189
  NoSelectStar.id: NoSelectStar,
186
190
  NoGenericSingleExportModule.id: NoGenericSingleExportModule,
187
191
  Stepdown.id: Stepdown,
@@ -193,19 +197,19 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
193
197
  PreferImmutableModuleConstant.id: PreferImmutableModuleConstant,
194
198
  MockWithoutSpec.id: MockWithoutSpec,
195
199
  OpaqueParametrizeCaseNeedsId.id: OpaqueParametrizeCaseNeedsId,
196
- FixtureReturnsBareTuple.id: FixtureReturnsBareTuple,
197
- GetDelegatesToGetMany.id: GetDelegatesToGetMany,
200
+ PytestFixtureReturnsBareTuple.id: PytestFixtureReturnsBareTuple,
201
+ StoreGetDelegatesToBulkRead.id: StoreGetDelegatesToBulkRead,
198
202
  PreferOneForRequiredRow.id: PreferOneForRequiredRow,
199
- KwargHeavyConstructionInTest.id: KwargHeavyConstructionInTest,
200
- DefectXfailRequiresStrict.id: DefectXfailRequiresStrict,
203
+ RepeatedKwargHeavyCallInTest.id: RepeatedKwargHeavyCallInTest,
204
+ DefectXfailRequiresExplicitStrict.id: DefectXfailRequiresExplicitStrict,
201
205
  NoFirstPartyPrivateImport.id: NoFirstPartyPrivateImport,
202
206
  NoRestatedComment.id: NoRestatedComment,
203
207
  RedundantDocstring.id: RedundantDocstring,
204
208
  TrailingValueNarration.id: TrailingValueNarration,
205
209
  NoStdlibLogging.id: NoStdlibLogging,
206
- NoGenRandomUuidInSql.id: NoGenRandomUuidInSql,
210
+ NoRandomUuidInSql.id: NoRandomUuidInSql,
207
211
  NoHiddenConstructorFallback.id: NoHiddenConstructorFallback,
208
- NoFileLevelEscapeHatchNoqa.id: NoFileLevelEscapeHatchNoqa,
212
+ NoFileLevelEscapeHatchSuppression.id: NoFileLevelEscapeHatchSuppression,
209
213
  NoFastapiOnEvent.id: NoFastapiOnEvent,
210
214
  NoOptionalTenantPredicate.id: NoOptionalTenantPredicate,
211
215
  NoTautologicalExpect.id: NoTautologicalExpect,
@@ -214,7 +218,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
214
218
  InvalidPydanticFieldDefault.id: InvalidPydanticFieldDefault,
215
219
  NoFrozenAfterValidatorFieldWrite.id: NoFrozenAfterValidatorFieldWrite,
216
220
  TriviallyTrueAssertion.id: TriviallyTrueAssertion,
217
- DuplicateTestBody.id: DuplicateTestBody,
221
+ NoRepeatedTestBody.id: NoRepeatedTestBody,
218
222
  UnusedMockSetup.id: UnusedMockSetup,
219
223
  PreferFstringOverConcat.id: PreferFstringOverConcat,
220
224
  PreferOrPattern.id: PreferOrPattern,
@@ -227,7 +231,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
227
231
  PreferSelfTypeAnnotation.id: PreferSelfTypeAnnotation,
228
232
  PreferSelfDocumentingConstant.id: PreferSelfDocumentingConstant,
229
233
  NoDuplicateDunderAllEntry.id: NoDuplicateDunderAllEntry,
230
- DuplicatedOverrideDocstring.id: DuplicatedOverrideDocstring,
234
+ NoCopiedInheritedDocstring.id: NoCopiedInheritedDocstring,
231
235
  RedundantClassDocstring.id: RedundantClassDocstring,
232
236
  RedundantModuleDocstring.id: RedundantModuleDocstring,
233
237
  DocstringArgsRestateSignature.id: DocstringArgsRestateSignature,
@@ -38,7 +38,7 @@ _GITHUB_ISSUE_RE = re.compile(r"https?://github\.com/[^/\s]+/[^/\s]+/issues/\d+\
38
38
  _NONDETERMINISM_RE = re.compile(r"intermittent|flak|sometimes|non-?deterministic|varies", re.IGNORECASE)
39
39
 
40
40
  # Sibling markers that declare a nondeterministic dependency.
41
- _NONDETERMINISTIC_MARKERS = frozenset({"real_llm", "flaky", "network", "integration"})
41
+ _NONDETERMINISTIC_MARKERS = frozenset({"real_llm", "flaky", "network"})
42
42
 
43
43
  # Hypothesis' entry point.
44
44
  _PROPERTY_DECORATORS = frozenset({"given"})
@@ -53,19 +53,23 @@ _FUNC_NODES = (ast.FunctionDef, ast.AsyncFunctionDef)
53
53
  _PYTESTMARK = "pytestmark"
54
54
 
55
55
 
56
- class DefectXfailRequiresStrict(Rule):
57
- id: str = "defect-xfail-requires-strict"
56
+ class DefectXfailRequiresExplicitStrict(Rule):
57
+ id: str = "defect-xfail-requires-explicit-strict"
58
58
  code: str = "SARJ046"
59
59
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
60
- summary="Bug-pinning `xfail` without `strict=True` an XPASS reports as a pass and the pin rots.",
61
- rationale="A non-strict defect pin stays green after the defect is fixed, leaving stale coverage and markers behind.",
60
+ summary="Defect-pinning `xfail` must explicitly set `strict=True` so an XPASS cannot silently pass.",
61
+ rationale=(
62
+ "A defect pin that relies on pytest's configurable strictness default can stay green after the defect is "
63
+ "fixed, leaving stale coverage and markers behind. Local strictness keeps the invariant attached to the pin."
64
+ ),
62
65
  remediation="Set `strict=True` so an unexpected pass fails and prompts removal of the obsolete marker.",
63
66
  category=RuleCategory.TESTING,
64
- aliases=("xfail-requires-strict",),
67
+ aliases=("defect-xfail-requires-strict", "xfail-requires-strict"),
65
68
  limitations=(
66
69
  "Only markers resolved through an unambiguous pytest or pytest.mark import are analyzed.",
67
70
  "Only xfail reasons that explicitly identify a defect are analyzed.",
68
- "Nondeterministic, property-based, integration, network, and environment-gated tests are excluded.",
71
+ "Nondeterministic, property-based, network, environment-gated, run=False, and statically disabled pins are excluded.",
72
+ "Repository-level strict_xfail / xfail_strict configuration is intentionally not consulted: defect pins remain locally explicit.",
69
73
  ),
70
74
  examples=(
71
75
  RuleExample(
@@ -134,6 +138,7 @@ def _rotting_bug_pins(tree: ast.Module, imports: ImportIndex) -> list[ast.Call]:
134
138
  hits.extend(_rotting_markers(node.decorator_list, imports))
135
139
  for node in nodes(tree, *_FUNC_NODES):
136
140
  hits.extend(_rotting_markers(node.decorator_list, imports))
141
+ hits.extend(_rotting_param_markers(node.decorator_list, imports))
137
142
  return hits
138
143
 
139
144
 
@@ -143,6 +148,18 @@ def _rotting_markers(markers: list[ast.expr], imports: ImportIndex) -> list[ast.
143
148
  return [marker for marker in markers if isinstance(marker, ast.Call) and _is_rotting_xfail(marker, imports)]
144
149
 
145
150
 
151
+ def _rotting_param_markers(decorators: list[ast.expr], imports: ImportIndex) -> list[ast.Call]:
152
+ if _has_nondeterministic_marker(decorators, imports):
153
+ return []
154
+ return [
155
+ nested
156
+ for decorator in decorators
157
+ if _pytest_marker_name(decorator, imports) == _PARAMETRIZE_ATTR
158
+ for nested in walk(decorator)
159
+ if isinstance(nested, ast.Call) and nested is not decorator and _is_rotting_xfail(nested, imports)
160
+ ]
161
+
162
+
146
163
  def _module_pytest_markers(tree: ast.Module, imports: ImportIndex) -> list[ast.expr]:
147
164
  values: list[ast.expr] = []
148
165
  for statement in tree.body:
@@ -191,7 +208,7 @@ def _pytest_marker_name(dec: ast.expr, imports: ImportIndex) -> str | None:
191
208
  def _is_rotting_xfail(dec: ast.expr, imports: ImportIndex) -> bool:
192
209
  if not isinstance(dec, ast.Call) or _pytest_marker_name(dec, imports) != _XFAIL:
193
210
  return False
194
- if _is_strict(dec) or _is_environment_gated(dec):
211
+ if _is_strict(dec) or _cannot_xpass(dec) or _is_environment_gated(dec):
195
212
  return False
196
213
  reason = _reason_text(dec)
197
214
  if reason is None or _NONDETERMINISM_RE.search(reason):
@@ -235,6 +252,15 @@ def _is_strict(dec: ast.Call) -> bool:
235
252
  return False
236
253
 
237
254
 
255
+ def _cannot_xpass(dec: ast.Call) -> bool:
256
+ for kw in dec.keywords:
257
+ if kw.arg == "run" and isinstance(kw.value, ast.Constant) and kw.value.value is False:
258
+ return True
259
+ if kw.arg == "condition" and isinstance(kw.value, ast.Constant) and kw.value.value is False:
260
+ return True
261
+ return bool(dec.args and isinstance(dec.args[0], ast.Constant) and dec.args[0].value is False)
262
+
263
+
238
264
  def _reason_text(dec: ast.Call) -> str | None:
239
265
  for kw in dec.keywords:
240
266
  if kw.arg == "reason":
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import PurePosixPath
5
+ import re
6
+ from typing import TYPE_CHECKING, ClassVar, final, override
7
+
8
+ from sarj_python_lint.rule_base import (
9
+ AutofixPolicy,
10
+ Diagnostic,
11
+ ExampleFile,
12
+ ExampleOutcome,
13
+ Rule,
14
+ RuleCategory,
15
+ RuleDocumentation,
16
+ RuleExample,
17
+ parse_or_none,
18
+ )
19
+ from sarj_python_lint.rules._paths import is_generated, is_test_path, is_test_support_path
20
+
21
+
22
+ if TYPE_CHECKING:
23
+ from pathlib import Path
24
+
25
+
26
+ _DOUBLE_NAME_RE = re.compile(r"^(?:Fake|Stub|Mock|InMemory|Recording|Scripted)[A-Z0-9]")
27
+ _SHARED_SUPPORT_NAMES = frozenset({"helpers", "support", "test_support", "test_utils"})
28
+
29
+
30
+ @final
31
+ class FakesInSharedLocation(Rule):
32
+ id = "fakes-in-shared-location"
33
+ code = "SARJ428"
34
+ documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
35
+ summary="Define reusable test doubles in a shared testing-support module.",
36
+ rationale=(
37
+ "A fake embedded in one test module silently becomes a private second implementation of a contract. "
38
+ "A named support module makes the double discoverable, reusable, and reviewable as test infrastructure."
39
+ ),
40
+ remediation=(
41
+ "Move the class to a testing, fakes, stubs, mocks, doubles, or test_utils module and import it. "
42
+ "Use an exact SARJ428 suppression only when the double is intentionally coupled to one scenario."
43
+ ),
44
+ category=RuleCategory.TESTING,
45
+ autofix=AutofixPolicy.NONE,
46
+ limitations=(
47
+ "Only top-level classes in test files with an explicit Fake, Stub, Mock, InMemory, Recording, or Scripted prefix are classified as doubles.",
48
+ "conftest.py and conventional shared-support paths are already shared locations and are excluded.",
49
+ ),
50
+ examples=(
51
+ RuleExample(
52
+ example_id="local-fake-service",
53
+ title="A test module privately defines a named fake",
54
+ outcome=ExampleOutcome.MATCH,
55
+ files=(
56
+ ExampleFile.python(
57
+ "tests/test_checkout.py",
58
+ "class FakePaymentGateway:\n async def charge(self, amount: int) -> None:\n pass\n",
59
+ ),
60
+ ),
61
+ focus_path=PurePosixPath("tests/test_checkout.py"),
62
+ expected_count=1,
63
+ public=True,
64
+ ),
65
+ RuleExample(
66
+ example_id="shared-fake-service",
67
+ title="A shared support module owns the fake",
68
+ outcome=ExampleOutcome.NO_MATCH,
69
+ files=(
70
+ ExampleFile.python(
71
+ "tests/fakes/payment.py",
72
+ "class FakePaymentGateway:\n async def charge(self, amount: int) -> None:\n pass\n",
73
+ ),
74
+ ),
75
+ focus_path=PurePosixPath("tests/fakes/payment.py"),
76
+ expected_count=0,
77
+ public=True,
78
+ ),
79
+ ),
80
+ )
81
+ description = documentation.summary
82
+
83
+ @override
84
+ def check(self, path: Path, source: str) -> list[Diagnostic]:
85
+ if not is_test_path(path) or _is_shared_support(path) or is_generated(path, source):
86
+ return []
87
+ tree = parse_or_none(path, source)
88
+ if tree is None:
89
+ return []
90
+ return [
91
+ Diagnostic(
92
+ path=path,
93
+ line=node.lineno,
94
+ col=node.col_offset + 1,
95
+ code=self.code,
96
+ message=(
97
+ f"test double `{node.name}` is private to this test module; move it to shared testing support "
98
+ "or add an exact SARJ428 suppression for a deliberately scenario-local double"
99
+ ),
100
+ )
101
+ for node in tree.body
102
+ if isinstance(node, ast.ClassDef) and _DOUBLE_NAME_RE.match(node.name)
103
+ ]
104
+
105
+
106
+ def _is_shared_support(path: Path) -> bool:
107
+ names = {part.lower() for part in path.parts}
108
+ return (
109
+ path.name == "conftest.py"
110
+ or path.stem.lower() in _SHARED_SUPPORT_NAMES
111
+ or bool(names & _SHARED_SUPPORT_NAMES)
112
+ or is_test_support_path(path)
113
+ )
@@ -44,6 +44,8 @@ _CONTAINERS = frozenset({"list", "List", "set", "Set", "tuple", "Tuple", "Sequen
44
44
  _BODY_MARKERS = frozenset({"Body", "Form", "File"})
45
45
  _NO_CONTENT_STATUSES = frozenset({204, 304})
46
46
  _STATUS_CODE_DIGITS = 3
47
+ _MIN_HTTP_STATUS = 100
48
+ _MAX_HTTP_STATUS = 599
47
49
  _DOCUMENTATION_EXAMPLE_DIR_NAMES = frozenset({"docs_src"})
48
50
  _CONVERTER_PATTERNS = MappingProxyType(
49
51
  {
@@ -62,14 +64,18 @@ class _Finding:
62
64
  message: str
63
65
 
64
66
 
65
- class FastapiOpenapiContract(Rule):
66
- id: str = "fastapi-openapi-contract"
67
+ class FastapiExplicitOpenapiContract(Rule):
68
+ id: str = "fastapi-explicit-openapi-contract"
67
69
  code: str = "SARJ094"
68
70
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
69
- summary="FastAPI operations must publish accurate request, response, and status contracts.",
70
- rationale="Typed routes and explicit response behavior keep generated OpenAPI accurate without duplicating self-documenting names in prose.",
71
+ summary="FastAPI operations must make request, response, and status contracts explicit in generated OpenAPI.",
72
+ rationale=(
73
+ "Locally explicit route metadata keeps generated OpenAPI reviewable and stable across framework defaults "
74
+ "without duplicating self-documenting names in prose."
75
+ ),
71
76
  remediation="Declare status codes, typed parameters, response schemas, and alternate responses raised directly by the handler.",
72
77
  category=RuleCategory.CORRECTNESS,
78
+ aliases=("fastapi-openapi-contract",),
73
79
  limitations=(
74
80
  "Hidden routes, WebSocket handlers, tests, generated files, documentation-source examples, and unrelated decorators are excluded.",
75
81
  "Dynamic response mappings are accepted when their contents cannot be resolved statically.",
@@ -134,8 +140,9 @@ class FastapiOpenapiContract(Rule):
134
140
  findings.extend(_check_raw_request(function, routes, index))
135
141
  findings.extend(_check_direct_responses(function, routes, index))
136
142
  for route in _operations(routes):
137
- findings.extend(_check_metadata(route))
143
+ findings.extend(_check_metadata(route, index))
138
144
  findings.extend(_check_projection(route))
145
+ findings.extend(_check_response_statuses(route, index))
139
146
  findings.extend(_check_route_conflicts(declared))
140
147
  return [
141
148
  Diagnostic(
@@ -155,11 +162,15 @@ def _operations(routes: tuple[Route, ...]) -> tuple[Route, ...]:
155
162
  return tuple({id(route.decorator): route for route in routes}.values())
156
163
 
157
164
 
158
- def _check_metadata(route: Route) -> list[_Finding]:
159
- if route.has_unpack:
160
- return []
165
+ def _check_metadata(route: Route, index: FastapiIndex) -> list[_Finding]:
161
166
  keywords = route.keywords
162
- if "status_code" in keywords and not _literal_none(keywords["status_code"]):
167
+ status_node = keywords.get("status_code")
168
+ if status_node is not None and not _literal_none(status_node):
169
+ status = _status_code(status_node, index)
170
+ if status is not None and not _valid_http_status(status):
171
+ return [_Finding(status_node, f"[status] {status} is outside the HTTP status range 100..599.")]
172
+ return []
173
+ if route.has_unpack:
163
174
  return []
164
175
  return [_Finding(route.decorator, "[metadata] operation requires explicit status_code.")]
165
176
 
@@ -454,6 +465,7 @@ def _check_direct_responses(
454
465
  index: FastapiIndex,
455
466
  ) -> list[_Finding]:
456
467
  statuses: set[int] = set()
468
+ invalid: dict[int, ast.Call] = {}
457
469
  stack: list[ast.AST] = list(function.body)
458
470
  while stack:
459
471
  current = stack.pop()
@@ -465,16 +477,25 @@ def _check_direct_responses(
465
477
  if isinstance(call, ast.Call) and index.is_http_exception(call.func):
466
478
  status = _status_code(_keyword(call, "status_code") or (call.args[0] if call.args else None), index)
467
479
  if status is not None:
468
- statuses.add(status)
480
+ if _valid_http_status(status):
481
+ statuses.add(status)
482
+ else:
483
+ invalid.setdefault(status, call)
469
484
  returned = current.value if isinstance(current, ast.Return) else None
470
485
  if isinstance(returned, ast.Call) and index.is_response(returned.func):
471
486
  status = _status_code(_keyword(returned, "status_code"), index)
472
487
  if status is not None:
473
- statuses.add(status)
488
+ if _valid_http_status(status):
489
+ statuses.add(status)
490
+ else:
491
+ invalid.setdefault(status, returned)
474
492
  stack.extend(children(current))
493
+ findings = [
494
+ _Finding(node, f"[status] {status} is outside the HTTP status range 100..599.")
495
+ for status, node in invalid.items()
496
+ ]
475
497
  if not statuses:
476
- return []
477
- findings: list[_Finding] = []
498
+ return findings
478
499
  for route in _operations(routes):
479
500
  responses = route.keywords.get("responses")
480
501
  if responses is not None and not isinstance(responses, ast.Dict):
@@ -492,6 +513,22 @@ def _check_direct_responses(
492
513
  return findings
493
514
 
494
515
 
516
+ def _check_response_statuses(route: Route, index: FastapiIndex) -> list[_Finding]:
517
+ responses = route.keywords.get("responses")
518
+ if not isinstance(responses, ast.Dict):
519
+ return []
520
+ findings: list[_Finding] = []
521
+ for key in responses.keys:
522
+ status = _status_code(key, index)
523
+ if key is not None and status is not None and not _valid_http_status(status):
524
+ findings.append(_Finding(key, f"[status] {status} is outside the HTTP status range 100..599."))
525
+ return findings
526
+
527
+
528
+ def _valid_http_status(status: int) -> bool:
529
+ return _MIN_HTTP_STATUS <= status <= _MAX_HTTP_STATUS
530
+
531
+
495
532
  def _status_code(node: ast.expr | None, index: FastapiIndex) -> int | None:
496
533
  if isinstance(node, ast.Constant) and isinstance(node.value, int):
497
534
  return node.value
@@ -33,7 +33,8 @@ class IacSourceCoupledTest(Rule):
33
33
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
34
34
  summary="Test asserts on raw IaC source text instead of a parsed plan, provider state, or runtime behavior.",
35
35
  rationale=(
36
- "Substring and regex checks can pass on comments, formatting, or unreachable Terraform configuration while clients fail silently."
36
+ "Substring and regex checks can pass on comments, formatting, or unreachable Terraform configuration "
37
+ "without proving the plan or deployed behavior."
37
38
  ),
38
39
  remediation="Parse rendered plan JSON, query the provider, or exercise the deployed runtime contract.",
39
40
  category=RuleCategory.TESTING,
@@ -45,7 +46,7 @@ class IacSourceCoupledTest(Rule):
45
46
  examples=(
46
47
  RuleExample(
47
48
  example_id="rendered-plan-contract",
48
- title="Assert on rendered Terraform plan behavior",
49
+ title="Assert on structured Terraform plan behavior",
49
50
  outcome=ExampleOutcome.NO_MATCH,
50
51
  files=(
51
52
  ExampleFile.python(