sarj-python-lint 0.84.3__tar.gz → 0.86.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/__main__.py +49 -13
  4. sarj_python_lint-0.86.0/src/sarj_python_lint/rules/_annotation_semantics.py +174 -0
  5. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_fastapi.py +3 -0
  6. sarj_python_lint-0.86.0/src/sarj_python_lint/rules/_fixed_record.py +578 -0
  7. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_imports.py +6 -0
  8. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_registry.py +8 -4
  9. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +32 -5
  10. sarj_python_lint-0.84.3/src/sarj_python_lint/rules/pydantic_at_boundaries.py → sarj_python_lint-0.86.0/src/sarj_python_lint/rules/named_record_at_boundaries.py +89 -29
  11. sarj_python_lint-0.86.0/src/sarj_python_lint/rules/no_any_mapping_types.py +302 -0
  12. sarj_python_lint-0.86.0/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +420 -0
  13. sarj_python_lint-0.86.0/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +306 -0
  14. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +59 -8
  15. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +65 -3
  16. sarj_python_lint-0.84.3/src/sarj_python_lint/rules/_fixed_record.py +0 -170
  17. sarj_python_lint-0.84.3/src/sarj_python_lint/rules/no_vague_annotations.py +0 -173
  18. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/.gitignore +0 -0
  19. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/LICENSE +0 -0
  20. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/README.md +0 -0
  21. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/__init__.py +0 -0
  22. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  23. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/_filesystem.py +0 -0
  24. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/_python_target.py +0 -0
  25. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  26. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/_secret_names.py +0 -0
  27. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/_version.py +0 -0
  28. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/py.typed +0 -0
  29. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/ratchet.py +0 -0
  30. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rule_base.py +0 -0
  31. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  32. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  33. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  34. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  35. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  36. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  37. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  38. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  39. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  40. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  41. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  42. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  43. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  44. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  45. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  46. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  47. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  48. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  49. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  50. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  51. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  52. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  53. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  54. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  55. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  56. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  57. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  58. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  59. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  60. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  61. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  62. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  63. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  64. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  65. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  66. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  67. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  68. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  69. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  70. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  71. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  72. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  73. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  74. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  75. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  76. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  77. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  78. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  79. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  80. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  81. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  82. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  83. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  84. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  85. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  86. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  87. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  88. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  89. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  90. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  91. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  92. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  93. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  94. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  95. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  96. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  97. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  98. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  99. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  100. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  101. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  102. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  103. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  104. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  105. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  106. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  107. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  108. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  109. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  110. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  111. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  112. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  113. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  114. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  115. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  116. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  117. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  118. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  119. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  120. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  121. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  122. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  123. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  124. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  125. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  126. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  127. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  128. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  129. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  130. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  131. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  132. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  133. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/require_typed_http_test_response.py +0 -0
  134. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  135. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  136. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  137. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  138. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  139. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  140. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  141. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  142. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  143. {sarj_python_lint-0.84.3 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 0.84.3
3
+ Version: 0.86.0
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
6
6
  Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "0.84.3"
3
+ version = "0.86.0"
4
4
  description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "sarj-ai" }]
@@ -142,7 +142,15 @@ _DIAGNOSTIC_PRECEDENCE = MappingProxyType(
142
142
  "SARJ088": frozenset({"SARJ050", "SARJ085", "SARJ420"}),
143
143
  "SARJ092": frozenset({"SARJ086", "SARJ087", "SARJ420"}),
144
144
  "SARJ093": frozenset({"SARJ034"}),
145
+ # Visible FastAPI operations have a route-aware contract diagnostic.
146
+ # Keep the annotation diagnostics when SARJ094 is absent, including
147
+ # when a concrete response_model makes the route contract complete.
148
+ "SARJ094": frozenset({"SARJ008", "SARJ447"}),
145
149
  "SARJ099": frozenset({"SARJ420"}),
150
+ # Any-valued mappings are the stronger correctness failure. SARJ008
151
+ # still owns every fixed dictionary return when selected alone, while
152
+ # an all-rules run emits one actionable diagnostic for this overlap.
153
+ "SARJ447": frozenset({"SARJ008"}),
146
154
  }
147
155
  )
148
156
 
@@ -162,13 +170,17 @@ def deduplicate_diagnostics(diags: list[Diagnostic], *, source: str | None = Non
162
170
  "SARJ420" in codes and not codes.isdisjoint(_DOCSTRING_PRECEDENCE_CODES)
163
171
  )
164
172
  docstring_owners = _docstring_owner_locations(source) if source is not None and needs_docstring_owners else {}
165
- needs_signature_owners = "SARJ093" in codes and "SARJ034" in codes
173
+ needs_signature_owners = (
174
+ ("SARJ093" in codes and "SARJ034" in codes)
175
+ or ("SARJ447" in codes and "SARJ008" in codes)
176
+ or ("SARJ094" in codes and not codes.isdisjoint(_DIAGNOSTIC_PRECEDENCE["SARJ094"]))
177
+ )
166
178
  signature_owners = (
167
179
  _function_signature_owner_locations(source) if source is not None and needs_signature_owners else {}
168
180
  )
169
181
 
170
182
  def owner_location(diagnostic: Diagnostic) -> _OwnerLocation:
171
- if diagnostic.code in {"SARJ034", "SARJ093"}:
183
+ if diagnostic.code in {"SARJ008", "SARJ034", "SARJ093", "SARJ094", "SARJ447"}:
172
184
  line, column = signature_owners.get(diagnostic.line, (diagnostic.line, diagnostic.col))
173
185
  else:
174
186
  line, column = docstring_owners.get(diagnostic.line, (diagnostic.line, diagnostic.col))
@@ -176,6 +188,8 @@ def deduplicate_diagnostics(diags: list[Diagnostic], *, source: str | None = Non
176
188
 
177
189
  present: dict[tuple[Path, int, int], dict[str, set[Severity]]] = {}
178
190
  for diagnostic in diags:
191
+ if diagnostic.code == "SARJ094" and not diagnostic.message.startswith("[return]"):
192
+ continue
179
193
  location = owner_location(diagnostic)
180
194
  by_code = present.setdefault((diagnostic.path, location.line, location.column), {})
181
195
  by_code.setdefault(diagnostic.code, set()).add(diagnostic.severity)
@@ -209,23 +223,45 @@ def _suppressed_diagnostics(
209
223
 
210
224
 
211
225
  def _suppressed_codes(codes: dict[str, set[Severity]]) -> set[tuple[str, Severity]]:
212
- suppressed = {
213
- (generic, generic_severity)
214
- for specific, generics in _DIAGNOSTIC_PRECEDENCE.items()
215
- if specific in codes
216
- for generic in generics
217
- for generic_severity in codes.get(generic, set())
218
- if generic_severity is Severity.WARNING or Severity.ERROR in codes[specific]
219
- }
226
+ suppressed = _precedence_suppressions(codes)
227
+ # SARJ094 is deliberately staged as a warning while teams calibrate the
228
+ # FastAPI contract policy. Its route-aware return finding still owns the
229
+ # overlapping annotation failure: emitting the blocking SARJ447 twin would
230
+ # defeat warning staging and give two remediations for one return contract.
231
+ if "SARJ094" in codes:
232
+ suppressed.update(_route_contract_suppressions(codes))
220
233
  # The promoted signature-restatement rules now block. Keep their errors and
221
234
  # drop only the lower-severity typed-docstring twin at the same owner.
222
- if Severity.WARNING in codes.get("SARJ092", set()) and any(
223
- Severity.ERROR in codes.get(generic, set()) for generic in ("SARJ086", "SARJ087")
224
- ):
235
+ if _typed_docstring_warning_is_superseded(codes):
225
236
  suppressed.add(("SARJ092", Severity.WARNING))
226
237
  return suppressed
227
238
 
228
239
 
240
+ def _precedence_suppressions(codes: dict[str, set[Severity]]) -> set[tuple[str, Severity]]:
241
+ suppressed: set[tuple[str, Severity]] = set()
242
+ for specific, generics in _DIAGNOSTIC_PRECEDENCE.items():
243
+ if specific not in codes:
244
+ continue
245
+ specific_severities = codes[specific]
246
+ for generic in generics:
247
+ for generic_severity in codes.get(generic, set()):
248
+ if generic_severity is Severity.WARNING or Severity.ERROR in specific_severities:
249
+ suppressed.add((generic, generic_severity))
250
+ return suppressed
251
+
252
+
253
+ def _route_contract_suppressions(codes: dict[str, set[Severity]]) -> set[tuple[str, Severity]]:
254
+ return {
255
+ (generic, severity) for generic in _DIAGNOSTIC_PRECEDENCE["SARJ094"] for severity in codes.get(generic, set())
256
+ }
257
+
258
+
259
+ def _typed_docstring_warning_is_superseded(codes: dict[str, set[Severity]]) -> bool:
260
+ if Severity.WARNING not in codes.get("SARJ092", set()):
261
+ return False
262
+ return any(Severity.ERROR in codes.get(generic, set()) for generic in ("SARJ086", "SARJ087"))
263
+
264
+
229
265
  def _function_signature_owner_locations(source: str) -> dict[int, _OwnerLocation]:
230
266
  try:
231
267
  tree = ast.parse(source)
@@ -0,0 +1,174 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass
5
+ from types import MappingProxyType
6
+ from typing import Self
7
+
8
+ from sarj_python_lint.rules._imports import ImportIndex
9
+
10
+
11
+ _TYPING_SOURCES = frozenset({"typing", "typing_extensions"})
12
+ _TRANSPARENT_WRAPPERS = frozenset({"Annotated", "ClassVar", "Final", "NotRequired", "Optional", "ReadOnly", "Required"})
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class AnnotationSemantics:
17
+ imports: ImportIndex
18
+ aliases: MappingProxyType[str, ast.expr]
19
+
20
+ @classmethod
21
+ def from_tree(cls, tree: ast.Module, *, module_scope_only: bool = True) -> Self:
22
+ imports = ImportIndex.from_tree(tree, module_scope_only=module_scope_only)
23
+ candidates: dict[str, ast.expr] = {}
24
+ ambiguous: set[str] = set()
25
+ for statement in tree.body:
26
+ name: str | None = None
27
+ value: ast.expr | None = None
28
+ match statement:
29
+ case ast.TypeAlias(name=ast.Name(id=alias), value=alias_value, type_params=[]):
30
+ name, value = alias, alias_value
31
+ case ast.AnnAssign(target=ast.Name(id=alias), annotation=annotation, value=alias_value) if (
32
+ alias_value is not None
33
+ and imports.resolves(annotation, sources=_TYPING_SOURCES, symbol="TypeAlias")
34
+ ):
35
+ name, value = alias, alias_value
36
+ case _:
37
+ pass
38
+ if name is None or value is None:
39
+ continue
40
+ if name in candidates:
41
+ ambiguous.add(name)
42
+ else:
43
+ candidates[name] = value
44
+ if not candidates:
45
+ return cls(imports, MappingProxyType({}))
46
+ rebound = _module_rebindings(tree)
47
+ aliases = {name: value for name, value in candidates.items() if name not in ambiguous and name not in rebound}
48
+ return cls(imports, MappingProxyType(aliases))
49
+
50
+ def parse(self, annotation: ast.expr | None) -> ast.expr | None:
51
+ if not isinstance(annotation, ast.Constant) or not isinstance(annotation.value, str):
52
+ return annotation
53
+ try:
54
+ parsed = ast.parse(annotation.value.strip(), mode="eval").body
55
+ except SyntaxError:
56
+ return None
57
+ ast.copy_location(parsed, annotation)
58
+ return parsed
59
+
60
+ def resolve_alias(self, annotation: ast.expr | None, *, seen: frozenset[str] = frozenset()) -> ast.expr | None:
61
+ resolved = self.parse(annotation)
62
+ while isinstance(resolved, ast.Name) and resolved.id in self.aliases and resolved.id not in seen:
63
+ seen |= {resolved.id}
64
+ resolved = self.parse(self.aliases[resolved.id])
65
+ return resolved
66
+
67
+ def transparent_members(
68
+ self, annotation: ast.expr | None, *, seen: frozenset[str] = frozenset()
69
+ ) -> tuple[ast.expr, ...]:
70
+ resolved = self.parse(annotation)
71
+ if resolved is None:
72
+ return ()
73
+ if isinstance(resolved, ast.Name) and resolved.id in self.aliases:
74
+ return self._alias_members(resolved.id, seen=seen)
75
+ if isinstance(resolved, ast.BinOp) and isinstance(resolved.op, ast.BitOr):
76
+ return self.transparent_members(resolved.left, seen=seen) + self.transparent_members(
77
+ resolved.right, seen=seen
78
+ )
79
+ if isinstance(resolved, ast.Subscript):
80
+ return self._subscript_members(resolved, seen=seen)
81
+ return (resolved,)
82
+
83
+ def _alias_members(self, name: str, *, seen: frozenset[str]) -> tuple[ast.expr, ...]:
84
+ if name in seen:
85
+ return ()
86
+ return self.transparent_members(self.aliases[name], seen=seen | {name})
87
+
88
+ def _subscript_members(self, node: ast.Subscript, *, seen: frozenset[str]) -> tuple[ast.expr, ...]:
89
+ wrapper = self.imports.resolved_symbol(node.value, sources=_TYPING_SOURCES)
90
+ if wrapper == "Union":
91
+ members = node.slice.elts if isinstance(node.slice, ast.Tuple) else [node.slice]
92
+ return self._flatten_members(members, seen=seen)
93
+ if wrapper not in _TRANSPARENT_WRAPPERS:
94
+ return (node,)
95
+ inner = node.slice.elts[0] if isinstance(node.slice, ast.Tuple) else node.slice
96
+ return self.transparent_members(inner, seen=seen)
97
+
98
+ def _flatten_members(self, nodes: list[ast.expr], *, seen: frozenset[str]) -> tuple[ast.expr, ...]:
99
+ members: list[ast.expr] = []
100
+ for node in nodes:
101
+ members.extend(self.transparent_members(node, seen=seen))
102
+ return tuple(members)
103
+
104
+
105
+ def _module_rebindings(tree: ast.Module) -> set[str]:
106
+ counts: dict[str, int] = {}
107
+ for statement in tree.body:
108
+ for name in _bound_names(statement):
109
+ counts[name] = counts.get(name, 0) + 1
110
+ return {name for name, count in counts.items() if count > 1}
111
+
112
+
113
+ def scope_bound_names(statements: list[ast.stmt]) -> set[str]:
114
+ return {name for statement in statements for name in _bound_names(statement)}
115
+
116
+
117
+ def _bound_names(statement: ast.stmt) -> set[str]:
118
+ collector = _ScopeBindings()
119
+ collector.visit(statement)
120
+ return collector.names
121
+
122
+
123
+ def _target_names(node: ast.expr) -> set[str]:
124
+ if isinstance(node, ast.Name):
125
+ return {node.id}
126
+ if isinstance(node, (ast.List, ast.Tuple)):
127
+ return {name for item in node.elts for name in _target_names(item)}
128
+ return set()
129
+
130
+
131
+ class _ScopeBindings(ast.NodeVisitor):
132
+ def __init__(self) -> None:
133
+ self.names: set[str] = set()
134
+
135
+ def visit_Name(self, node: ast.Name) -> None:
136
+ if isinstance(node.ctx, (ast.Store, ast.Del)):
137
+ self.names.add(node.id)
138
+
139
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
140
+ self.names.add(node.name)
141
+
142
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
143
+ self.names.add(node.name)
144
+
145
+ def visit_ClassDef(self, node: ast.ClassDef) -> None:
146
+ self.names.add(node.name)
147
+
148
+ def visit_Lambda(self, node: ast.Lambda) -> None:
149
+ pass
150
+
151
+ def visit_Import(self, node: ast.Import) -> None:
152
+ self.names.update(alias.asname or alias.name.partition(".")[0] for alias in node.names)
153
+
154
+ def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
155
+ self.names.update(alias.asname or alias.name for alias in node.names if alias.name != "*")
156
+
157
+ def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None:
158
+ if node.name is not None:
159
+ self.names.add(node.name)
160
+ self.generic_visit(node)
161
+
162
+ def visit_MatchAs(self, node: ast.MatchAs) -> None:
163
+ if node.name is not None:
164
+ self.names.add(node.name)
165
+ self.generic_visit(node)
166
+
167
+ def visit_MatchStar(self, node: ast.MatchStar) -> None:
168
+ if node.name is not None:
169
+ self.names.add(node.name)
170
+
171
+ def visit_MatchMapping(self, node: ast.MatchMapping) -> None:
172
+ if node.rest is not None:
173
+ self.names.add(node.rest)
174
+ self.generic_visit(node)
@@ -5,6 +5,8 @@ from collections import Counter
5
5
  from dataclasses import dataclass
6
6
  from typing import TYPE_CHECKING, Literal, NamedTuple
7
7
 
8
+ from sarj_python_lint.rules._imports import ImportIndex
9
+
8
10
 
9
11
  if TYPE_CHECKING:
10
12
  from pathlib import Path
@@ -107,6 +109,7 @@ class FastapiIndex:
107
109
  def __init__(self, tree: ast.Module, *, path: Path | None = None) -> None:
108
110
  self.tree: ast.Module = tree
109
111
  self.path: Path | None = path
112
+ self.imports: ImportIndex = ImportIndex.from_tree(tree, module_scope_only=True)
110
113
  self._imported_dependency_cache: dict[_ImportedReference, bool] = {}
111
114
  self._imported_module_cache: dict[Path, ast.Module | None] = {}
112
115
  self.modules: set[str] = set()