sarj-python-lint 1.42.0__tar.gz → 1.42.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 (207) hide show
  1. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/PKG-INFO +2 -2
  2. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/pyproject.toml +2 -2
  3. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_annotation_semantics.py +9 -0
  4. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_fastapi.py +4 -5
  5. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_pydantic_field_contract.py +38 -29
  6. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +6 -6
  7. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +6 -6
  8. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/iac_source_coupled_test.py +1 -1
  9. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +19 -3
  10. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/named_record_at_boundaries.py +4 -6
  11. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +39 -2
  12. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_database_programmability.py +2 -2
  13. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_loguru_exc_info.py +1 -1
  14. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_loguru_printf_format.py +1 -1
  15. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +16 -34
  16. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_redundant_literal_description.py +18 -3
  17. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_repeated_string_literal.py +13 -11
  18. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_secret_in_log.py +1 -1
  19. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_tautological_expect.py +40 -4
  20. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +4 -6
  21. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_class_row.py +1 -1
  22. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +17 -10
  23. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +10 -0
  24. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +2 -1
  25. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +1 -1
  26. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/repeated_static_call_cases.py +1 -1
  27. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/unused_test_factory_option.py +1 -1
  28. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/.gitignore +0 -0
  29. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/LICENSE +0 -0
  30. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/README.md +0 -0
  31. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/__init__.py +0 -0
  32. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/__main__.py +0 -0
  33. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/_analysis_session.py +0 -0
  34. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/_file_context.py +0 -0
  35. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/_filesystem.py +0 -0
  36. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/_python_target.py +0 -0
  37. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  38. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/_secret_names.py +0 -0
  39. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/_source.py +0 -0
  40. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/_version.py +0 -0
  41. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/benchmark.py +0 -0
  42. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/interpreter_argv.py +0 -0
  43. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/json_boundary.py +0 -0
  44. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/py.typed +0 -0
  45. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/ratchet.py +0 -0
  46. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rule_base.py +0 -0
  47. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/__init__.py +0 -0
  48. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  49. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  50. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  51. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_comments.py +0 -0
  52. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  53. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  54. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  55. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  56. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_exports.py +0 -0
  57. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_first_party.py +0 -0
  58. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  59. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_imports.py +0 -0
  60. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  61. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_local_source.py +0 -0
  62. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_logging.py +0 -0
  63. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  64. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  65. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_paths.py +0 -0
  66. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_project_index.py +0 -0
  67. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  68. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_pytest.py +0 -0
  69. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_registry.py +0 -0
  70. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  71. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  72. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  73. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_sql.py +0 -0
  74. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  75. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  76. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  77. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  78. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  79. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  80. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  81. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  82. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  83. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  84. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  85. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  86. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  87. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  88. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  89. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  90. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  91. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  92. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  93. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  94. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  95. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  96. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  97. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  98. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  99. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  100. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  101. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  102. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  103. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  104. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  105. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  106. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  107. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  108. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  109. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  110. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  111. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  112. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  113. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  114. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  115. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  116. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  117. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  118. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  119. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  120. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  121. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  122. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  123. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  124. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  125. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  126. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  127. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  128. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  129. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  130. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  131. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  132. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  133. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  134. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  135. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  136. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  137. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  138. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  139. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  140. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  141. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  142. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  143. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  144. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  145. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  146. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  147. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  148. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  149. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  150. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  151. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  152. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  153. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  154. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  155. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  156. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  157. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  158. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  159. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  160. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  161. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  162. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  163. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  164. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  165. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  166. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  167. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  168. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  169. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  170. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  171. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  172. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  173. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  174. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  175. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  176. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  177. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  178. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  179. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  180. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  181. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  182. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  183. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  184. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  185. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  186. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  187. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  188. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  189. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  190. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  191. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  192. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  193. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  194. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  195. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  196. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  197. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  198. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/stepdown.py +0 -0
  199. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  200. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  201. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  202. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  203. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  204. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  205. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  206. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.2}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  207. {sarj_python_lint-1.42.0 → sarj_python_lint-1.42.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: 1.42.0
3
+ Version: 1.42.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/
@@ -19,7 +19,7 @@ Classifier: Topic :: Software Development :: Quality Assurance
19
19
  Requires-Python: >=3.14
20
20
  Requires-Dist: packaging==26.3
21
21
  Requires-Dist: ruff>=0.17.0
22
- Requires-Dist: sarj-rule-contracts==0.2.4
22
+ Requires-Dist: sarj-rule-contracts==0.2.5
23
23
  Requires-Dist: sqlglot==30.22.0
24
24
  Requires-Dist: typer==0.27.3
25
25
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.42.0"
3
+ version = "1.42.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" }]
@@ -15,7 +15,7 @@ classifiers = [
15
15
  "Programming Language :: Python :: 3.14",
16
16
  "Topic :: Software Development :: Quality Assurance",
17
17
  ]
18
- dependencies = ["sarj-rule-contracts==0.2.4", "packaging==26.3", "ruff>=0.17.0", "sqlglot==30.22.0", "typer==0.27.3"]
18
+ dependencies = ["sarj-rule-contracts==0.2.5", "packaging==26.3", "ruff>=0.17.0", "sqlglot==30.22.0", "typer==0.27.3"]
19
19
 
20
20
  [project.scripts]
21
21
  sarj-python-lint = "sarj_python_lint.__main__:main"
@@ -131,6 +131,15 @@ def _target_names(node: ast.expr) -> set[str]:
131
131
  class _ScopeBindings(ast.NodeVisitor):
132
132
  def __init__(self) -> None:
133
133
  self.names: set[str] = set()
134
+ self._pending: list[ast.AST] = []
135
+
136
+ def visit(self, node: ast.AST) -> None:
137
+ self._pending.append(node)
138
+ while self._pending:
139
+ super().visit(self._pending.pop())
140
+
141
+ def generic_visit(self, node: ast.AST) -> None:
142
+ self._pending.extend(reversed(tuple(ast.iter_child_nodes(node))))
134
143
 
135
144
  def visit_Name(self, node: ast.Name) -> None:
136
145
  if isinstance(node.ctx, (ast.Store, ast.Del)):
@@ -138,7 +138,9 @@ class FastapiIndex:
138
138
  self._read_aliases(tree)
139
139
 
140
140
  def _index_scopes(self, tree: ast.Module) -> None:
141
- def visit(node: ast.AST, scope: int, class_owner: int | None) -> None:
141
+ pending: list[tuple[ast.AST, int, int | None]] = [(tree, 0, None)]
142
+ while pending:
143
+ node, scope, class_owner = pending.pop()
142
144
  self._node_scopes[id(node)] = scope
143
145
  if isinstance(node, ast.ClassDef):
144
146
  class_owner = node.lineno
@@ -149,10 +151,7 @@ class FastapiIndex:
149
151
  self._route_scopes[id(node)] = scope
150
152
  self._scope_parents[node.lineno] = scope
151
153
  scope = node.lineno
152
- for child in ast.iter_child_nodes(node):
153
- visit(child, scope, class_owner)
154
-
155
- visit(tree, 0, None)
154
+ pending.extend((child, scope, class_owner) for child in reversed(tuple(ast.iter_child_nodes(node))))
156
155
 
157
156
  def _read_imports(self, tree: ast.Module) -> None:
158
157
  for node in walk_ast(tree):
@@ -108,39 +108,48 @@ def inert_signature(node: ast.FunctionDef | ast.AsyncFunctionDef, context: Pytho
108
108
 
109
109
 
110
110
  def _inert_expr(node: ast.AST, context: PythonFileContext) -> bool:
111
- if isinstance(node, ast.Subscript) and not _inert_subscription(node, context):
112
- return False
113
- if isinstance(node, ast.Lambda):
114
- return not node.args.defaults and not node.args.kw_defaults
115
- if isinstance(node, ast.Call):
116
- target = context.imports.resolved_qualified_name(node.func)
117
- if target not in {
118
- "pydantic.Field",
119
- "pydantic.fields.Field",
120
- "pydantic.ConfigDict",
121
- "pydantic.config.ConfigDict",
122
- }:
111
+ pending = [node]
112
+ while pending:
113
+ current = pending.pop()
114
+ if isinstance(current, ast.Subscript) and not _inert_subscription(current, context):
115
+ return False
116
+ if isinstance(current, ast.Lambda):
117
+ if current.args.defaults or current.args.kw_defaults:
118
+ return False
119
+ continue
120
+ if isinstance(current, ast.Call) and not _inert_pydantic_call(current, context):
123
121
  return False
124
- if any(keyword.arg is None for keyword in node.keywords) or any(
125
- isinstance(argument, ast.Starred) for argument in node.args
122
+ if isinstance(
123
+ current,
124
+ (
125
+ ast.NamedExpr,
126
+ ast.Starred,
127
+ ast.Await,
128
+ ast.Yield,
129
+ ast.YieldFrom,
130
+ ast.ListComp,
131
+ ast.SetComp,
132
+ ast.DictComp,
133
+ ast.GeneratorExp,
134
+ ),
126
135
  ):
127
136
  return False
128
- if isinstance(
129
- node,
130
- (
131
- ast.NamedExpr,
132
- ast.Starred,
133
- ast.Await,
134
- ast.Yield,
135
- ast.YieldFrom,
136
- ast.ListComp,
137
- ast.SetComp,
138
- ast.DictComp,
139
- ast.GeneratorExp,
140
- ),
141
- ):
137
+ pending.extend(reversed(tuple(ast.iter_child_nodes(current))))
138
+ return True
139
+
140
+
141
+ def _inert_pydantic_call(node: ast.Call, context: PythonFileContext) -> bool:
142
+ target = context.imports.resolved_qualified_name(node.func)
143
+ if target not in {
144
+ "pydantic.Field",
145
+ "pydantic.fields.Field",
146
+ "pydantic.ConfigDict",
147
+ "pydantic.config.ConfigDict",
148
+ }:
142
149
  return False
143
- return all(_inert_expr(child, context) for child in ast.iter_child_nodes(node))
150
+ return not any(keyword.arg is None for keyword in node.keywords) and not any(
151
+ isinstance(argument, ast.Starred) for argument in node.args
152
+ )
144
153
 
145
154
 
146
155
  def _inert_subscription(node: ast.Subscript, context: PythonFileContext) -> bool:
@@ -128,14 +128,14 @@ class DocstringArgsRestateSignature(Rule):
128
128
  imports: ImportIndex,
129
129
  diags: list[Diagnostic],
130
130
  ) -> None:
131
- for child in children(node):
131
+ pending: list[tuple[ast.AST, str | None]] = [(child, class_name) for child in reversed(children(node))]
132
+ while pending:
133
+ child, owner = pending.pop()
132
134
  if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
133
- self._check_function(child, class_name, path, imports, diags)
134
- self._walk(child, class_name, path, imports, diags)
135
+ self._check_function(child, owner, path, imports, diags)
135
136
  elif isinstance(child, ast.ClassDef):
136
- self._walk(child, child.name, path, imports, diags)
137
- else:
138
- self._walk(child, class_name, path, imports, diags)
137
+ owner = child.name
138
+ pending.extend((nested, owner) for nested in reversed(children(child)))
139
139
 
140
140
  def _check_function(
141
141
  self,
@@ -196,14 +196,14 @@ class DocstringReturnsRestateSignature(Rule):
196
196
  imports: ImportIndex,
197
197
  diags: list[Diagnostic],
198
198
  ) -> None:
199
- for child in children(node):
199
+ pending: list[tuple[ast.AST, str | None]] = [(child, class_name) for child in reversed(children(node))]
200
+ while pending:
201
+ child, owner = pending.pop()
200
202
  if isinstance(child, ast.FunctionDef | ast.AsyncFunctionDef):
201
- self._check_function(child, class_name, path, imports, diags)
202
- self._walk(child, class_name, path, imports, diags)
203
+ self._check_function(child, owner, path, imports, diags)
203
204
  elif isinstance(child, ast.ClassDef):
204
- self._walk(child, child.name, path, imports, diags)
205
- else:
206
- self._walk(child, class_name, path, imports, diags)
205
+ owner = child.name
206
+ pending.extend((nested, owner) for nested in reversed(children(child)))
207
207
 
208
208
  def _check_function(
209
209
  self,
@@ -62,7 +62,7 @@ class IacSourceCoupledTest(Rule):
62
62
  files=(
63
63
  ExampleFile.python(
64
64
  "tests/test_policy.py",
65
- "def test_policy(rendered_plan_json: str):\n"
65
+ "import json\n\ndef test_policy(rendered_plan_json: str):\n"
66
66
  " plan = json.loads(rendered_plan_json)\n"
67
67
  " changes = plan['resource_changes']\n"
68
68
  " assert changes[0]['change']['actions'] == ['create']\n",
@@ -314,12 +314,24 @@ def _literal_key(value: object) -> _LiteralKey:
314
314
  return _LiteralKey(type(value), value)
315
315
 
316
316
 
317
+ def _bit_union_members(node: ast.expr) -> list[ast.expr]:
318
+ members: list[ast.expr] = []
319
+ pending = [node]
320
+ while pending:
321
+ member = pending.pop()
322
+ if isinstance(member, ast.BinOp) and isinstance(member.op, ast.BitOr):
323
+ pending.extend((member.right, member.left))
324
+ else:
325
+ members.append(member)
326
+ return members
327
+
328
+
317
329
  def _literal_domain(node: ast.expr, imports: ImportIndex) -> _LiteralDomain | None:
318
330
  node = _parse_forward_annotation(node)
319
331
  if isinstance(node, ast.Constant) and node.value is None:
320
332
  return _LiteralDomain(frozenset({_literal_key(None)}))
321
333
  if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
322
- return _merge_domains((_literal_domain(node.left, imports), _literal_domain(node.right, imports)))
334
+ return _merge_domains(tuple(_literal_domain(member, imports) for member in _bit_union_members(node)))
323
335
  if not isinstance(node, ast.Subscript):
324
336
  return None
325
337
  if imports.resolves(node.value, sources=_TYPING_SOURCES, symbol="Annotated"):
@@ -334,8 +346,12 @@ def _literal_domain(node: ast.expr, imports: ImportIndex) -> _LiteralDomain | No
334
346
  return _merge_domains(tuple(_literal_domain(member, imports) for member in _slice_members(node.slice)))
335
347
  if not imports.resolves(node.value, sources=_TYPING_SOURCES, symbol="Literal"):
336
348
  return None
349
+ return _literal_members_domain(_slice_members(node.slice))
350
+
351
+
352
+ def _literal_members_domain(members: tuple[ast.expr, ...]) -> _LiteralDomain | None:
337
353
  values: set[_LiteralKey] = set()
338
- for member in _slice_members(node.slice):
354
+ for member in members:
339
355
  value = _literal_value(member)
340
356
  if value is _MISSING or isinstance(value, float):
341
357
  return None
@@ -514,7 +530,7 @@ def _numeric_bound_is_violated(name: str, default: float, bound: float) -> bool:
514
530
  def _provably_non_null(node: ast.expr, imports: ImportIndex) -> bool:
515
531
  node = _parse_forward_annotation(node)
516
532
  if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
517
- return _provably_non_null(node.left, imports) and _provably_non_null(node.right, imports)
533
+ return all(_provably_non_null(member, imports) for member in _bit_union_members(node))
518
534
  if isinstance(node, ast.Name):
519
535
  return node.id in _KNOWN_NON_NULL_BUILTINS and imports.builtin_is_unshadowed(node.id)
520
536
  if not isinstance(node, ast.Subscript):
@@ -237,16 +237,14 @@ def _is_framework_hook(node: ast.FunctionDef | ast.AsyncFunctionDef, imports: Im
237
237
 
238
238
  def _private_class_function_ids(tree: ast.Module) -> set[int]:
239
239
  private: set[int] = set()
240
-
241
- def visit(node: ast.AST, *, inside_private_class: bool) -> None:
240
+ pending: list[tuple[ast.AST, bool]] = [(tree, False)]
241
+ while pending:
242
+ node, inside_private_class = pending.pop()
242
243
  if isinstance(node, ast.ClassDef):
243
244
  inside_private_class = inside_private_class or node.name.startswith("_")
244
245
  if inside_private_class and isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
245
246
  private.add(id(node))
246
- for child in children(node):
247
- visit(child, inside_private_class=inside_private_class)
248
-
249
- visit(tree, inside_private_class=False)
247
+ pending.extend((child, inside_private_class) for child in reversed(children(node)))
250
248
  return private
251
249
 
252
250
 
@@ -2,7 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  import ast
4
4
  from dataclasses import replace
5
+ from io import StringIO
5
6
  from pathlib import PurePosixPath
7
+ import tokenize
6
8
  from types import MappingProxyType
7
9
  from typing import TYPE_CHECKING, ClassVar, final
8
10
 
@@ -187,8 +189,10 @@ class NoBroadKeywordCapture(Rule):
187
189
  overloads = _overload_implementations(context, semantics, mutated_members)
188
190
  if function in overloads:
189
191
  continue
190
- if is_suppressed(context.source_lines, argument.lineno, self.code) or is_suppressed(
191
- context.source_lines, function.lineno, self.code
192
+ if (
193
+ is_suppressed(context.source_lines, argument.lineno, self.code)
194
+ or is_suppressed(context.source_lines, function.lineno, self.code)
195
+ or _signature_end_suppressed(function, context, self.code)
192
196
  ):
193
197
  continue
194
198
  findings.append(
@@ -207,6 +211,39 @@ class NoBroadKeywordCapture(Rule):
207
211
  return sorted(findings, key=lambda finding: (finding.line, finding.col))
208
212
 
209
213
 
214
+ def _signature_end_suppressed(
215
+ function: ast.FunctionDef | ast.AsyncFunctionDef, context: PythonFileContext, code: str
216
+ ) -> bool:
217
+ first_statement = function.body[0]
218
+ header = context.source_lines[function.lineno - 1 : first_statement.lineno]
219
+ if not any("sarj-noqa" in line.lower() for line in header):
220
+ return False
221
+ # AST columns count UTF-8 bytes, and a continued inline body can share the header's logical line.
222
+ header[-1] = header[-1].encode("utf-8")[: first_statement.col_offset].decode("utf-8")
223
+ try:
224
+ line = _signature_end_line(header, function.lineno)
225
+ except tokenize.TokenError, SyntaxError:
226
+ return False
227
+ return line is not None and is_suppressed(context.source_lines, line, code)
228
+
229
+
230
+ def _signature_end_line(header: list[str], start_line: int) -> int | None:
231
+ depth = 0
232
+ line = None
233
+ for token in tokenize.generate_tokens(StringIO("\n".join(header)).readline):
234
+ if token.type == tokenize.NEWLINE:
235
+ return line
236
+ if token.type != tokenize.OP:
237
+ continue
238
+ if token.string in {"(", "[", "{"}:
239
+ depth += 1
240
+ elif token.string in {")", "]", "}"}:
241
+ depth -= 1
242
+ elif token.string == ":" and depth == 0:
243
+ line = start_line + token.start[0] - 1
244
+ return line
245
+
246
+
210
247
  def _interface_severity(
211
248
  function: ast.FunctionDef | ast.AsyncFunctionDef,
212
249
  context: PythonFileContext,
@@ -29,8 +29,8 @@ _DATABASE_BEHAVIOR = re.compile(
29
29
  r"CREATE\s+CONSTRAINT\s+TRIGGER|ALTER\s+TABLE\s+[^;]+?\s+ENABLE\s+(?:(?:ALWAYS|REPLICA)\s+)?TRIGGER)\b",
30
30
  re.IGNORECASE,
31
31
  )
32
- _BAD = 'await conn.execute("CREATE TRIGGER fail_counter BEFORE UPDATE ON batch EXECUTE FUNCTION fail_counter()")\n'
33
- _GOOD = 'await conn.execute("ALTER TABLE batch ADD CONSTRAINT fail_counter CHECK (false) NOT VALID")\n'
32
+ _BAD = 'async def install_fault(conn):\n await conn.execute("CREATE TRIGGER fail_counter BEFORE UPDATE ON batch EXECUTE FUNCTION fail_counter()")\n'
33
+ _GOOD = 'async def install_fault(conn):\n await conn.execute("ALTER TABLE batch ADD CONSTRAINT fail_counter CHECK (false) NOT VALID")\n'
34
34
 
35
35
 
36
36
  @final
@@ -83,7 +83,7 @@ class NoLoguruExcInfo(Rule):
83
83
 
84
84
  @override
85
85
  def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
86
- if "exc_info" not in context.source or context.generated or context.tree is None:
86
+ if "exc_info" not in context.symbol_source or context.generated or context.tree is None:
87
87
  return []
88
88
  shadowed = _ambiguous_import_names(context)
89
89
  if "*" in shadowed:
@@ -89,7 +89,7 @@ class NoLoguruPrintfFormat(Rule):
89
89
 
90
90
  @override
91
91
  def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
92
- if "loguru" not in context.source or context.generated or context.tree is None:
92
+ if "loguru" not in context.symbol_source or context.generated or context.tree is None:
93
93
  return []
94
94
  provenance: ResourceProvenance | None = None
95
95
  diagnostics: list[Diagnostic] = []
@@ -386,40 +386,22 @@ def _has_dynamic_reflective_self_write(function: ast.FunctionDef | ast.AsyncFunc
386
386
 
387
387
  def _function_scopes(tree: ast.Module) -> list[_FunctionScope]:
388
388
  scopes: list[_FunctionScope] = []
389
-
390
- class Collector(ast.NodeVisitor):
391
- def __init__(self) -> None:
392
- self.owner: ast.ClassDef | None = None
393
-
394
- @override
395
- def visit_ClassDef(self, node: ast.ClassDef) -> None:
396
- previous = self.owner
397
- self.owner = node
398
- for statement in node.body:
399
- self.visit(statement)
400
- self.owner = previous
401
-
402
- @override
403
- def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
404
- self._function(node)
405
-
406
- @override
407
- def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
408
- self._function(node)
409
-
410
- def _function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
411
- scopes.append(_FunctionScope(node, self.owner))
412
- previous = self.owner
413
- self.owner = None
414
- for statement in node.body:
415
- self.visit(statement)
416
- self.owner = previous
417
-
418
- @override
419
- def visit_Lambda(self, node: ast.Lambda) -> None:
420
- return
421
-
422
- Collector().visit(tree)
389
+ pending: list[tuple[ast.AST, ast.ClassDef | None]] = [(tree, None)]
390
+ while pending:
391
+ node, owner = pending.pop()
392
+ match node:
393
+ case ast.Lambda():
394
+ continue
395
+ case ast.ClassDef():
396
+ owner = node
397
+ children = tuple(node.body)
398
+ case ast.FunctionDef() | ast.AsyncFunctionDef():
399
+ scopes.append(_FunctionScope(node, owner))
400
+ owner = None
401
+ children = tuple(node.body)
402
+ case _:
403
+ children = tuple(ast.iter_child_nodes(node))
404
+ pending.extend((child, owner) for child in reversed(children))
423
405
  return scopes
424
406
 
425
407
 
@@ -275,9 +275,7 @@ def _closed_domain(
275
275
  if isinstance(node, ast.Name):
276
276
  return local_domains.get(node.id)
277
277
  if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
278
- left = _closed_domain(node.left, imports, local_domains)
279
- right = _closed_domain(node.right, imports, local_domains)
280
- return left | right if left is not None and right is not None else None
278
+ return _closed_union_domain(node, imports, local_domains)
281
279
  if not isinstance(node, ast.Subscript):
282
280
  return None
283
281
  if imports.resolves(node.value, sources=_TYPING_SOURCES, symbol="Literal"):
@@ -288,6 +286,23 @@ def _closed_domain(
288
286
  return None
289
287
 
290
288
 
289
+ def _closed_union_domain(
290
+ node: ast.expr, imports: ImportIndex, local_domains: dict[str, frozenset[str]]
291
+ ) -> frozenset[str] | None:
292
+ values: set[str] = set()
293
+ pending = [node]
294
+ while pending:
295
+ member = pending.pop()
296
+ if isinstance(member, ast.BinOp) and isinstance(member.op, ast.BitOr):
297
+ pending.extend((member.right, member.left))
298
+ continue
299
+ domain = _closed_domain(member, imports, local_domains)
300
+ if domain is None:
301
+ return None
302
+ values.update(domain)
303
+ return frozenset(values)
304
+
305
+
291
306
  def _repeats_domain(description: str, domain: frozenset[str]) -> bool:
292
307
  match = _DOMAIN_CLAUSE.search(description)
293
308
  if match is None:
@@ -154,17 +154,19 @@ class NoRepeatedStringLiteral(Rule):
154
154
  scope_of[id(node)] = scope
155
155
 
156
156
  def visit(node: ast.AST, scope: int) -> None:
157
- for annotation in _annotation_exprs(node):
158
- excluded.update(id(child) for child in walk(annotation) if isinstance(child, ast.Constant))
159
- if isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
160
- scope = declaration_scope(node, scope)
161
- elif isinstance(node, ast.Lambda):
162
- scope = id(node)
163
- scope_line_of[scope] = node.lineno
164
- else:
165
- collect_literal(node, scope)
166
- for child in children(node):
167
- visit(child, scope)
157
+ pending = [(node, scope)]
158
+ while pending:
159
+ node, scope = pending.pop()
160
+ for annotation in _annotation_exprs(node):
161
+ excluded.update(id(child) for child in walk(annotation) if isinstance(child, ast.Constant))
162
+ if isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
163
+ scope = declaration_scope(node, scope)
164
+ elif isinstance(node, ast.Lambda):
165
+ scope = id(node)
166
+ scope_line_of[scope] = node.lineno
167
+ else:
168
+ collect_literal(node, scope)
169
+ pending.extend((child, scope) for child in reversed(children(node)))
168
170
 
169
171
  visit(tree, _MODULE_SCOPE)
170
172
 
@@ -103,7 +103,7 @@ class NoSecretInLog(Rule):
103
103
  ),
104
104
  RuleExample(
105
105
  example_id="redacted-logging-keyword",
106
- title="Token prefix logged under a redacted name",
106
+ title="Only non-sensitive authentication metadata is logged",
107
107
  outcome=ExampleOutcome.NO_MATCH,
108
108
  files=(
109
109
  ExampleFile.python(
@@ -40,24 +40,56 @@ class NoTautologicalExpect(Rule):
40
40
  id: str = "no-statically-truthy-assertion"
41
41
  code: str = "SARJ057"
42
42
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
43
- summary="An assertion condition is statically truthy, including test-local scalar setup.",
43
+ summary="An assertion condition is statically truthy, including deferred expressions and test-local scalar setup.",
44
44
  rationale=(
45
45
  "A condition that stays truthy whenever evaluation succeeds cannot reject an incorrect result and often "
46
- "means the intended condition was wrapped in a container or placed in the message slot."
46
+ "means the intended condition was wrapped in a container, deferred without execution, or placed in the message slot."
47
47
  ),
48
48
  remediation=(
49
49
  "Assert an explicit runtime-derived condition. If the test only requires an operation not to raise, "
50
- "evaluate it directly and retain any meaningful follow-up assertion."
50
+ "evaluate it directly and retain any meaningful follow-up assertion. Consume generator values or call "
51
+ "the lambda when their result is the contract; choose empty-input behavior explicitly."
51
52
  ),
52
53
  category=RuleCategory.TESTING,
53
54
  aliases=("no-tautological-expect",),
54
55
  limitations=(
55
- "Detection covers truthy scalar constants and definitely non-empty list, set, and dict displays.",
56
+ "Detection covers truthy scalar constants, definitely non-empty list/set/dict displays, and directly asserted generator expressions or lambdas.",
57
+ "Generator consumption, lambda invocation, comprehensions, and runtime iterator/callable values are not inferred; empty universal-quantification contracts remain valid.",
56
58
  "Test-local immutable scalar bindings and their aliases are tracked only through straight-line assignments; calls, control flow, mutable values, and global/nonlocal writes invalidate tracking.",
57
59
  "Ruff owns asserted strings and tuples, literal comparisons and identity, and unittest-style assertion methods.",
58
60
  "Generated files, always-failing assertions, deliberate match-arm success markers, and runtime-value comparisons are excluded.",
59
61
  ),
60
62
  examples=(
63
+ RuleExample(
64
+ example_id="unconsumed-generator",
65
+ scenario="deferred-condition",
66
+ title="A generator object does not check the yielded conditions",
67
+ outcome=ExampleOutcome.MATCH,
68
+ files=(
69
+ ExampleFile.python(
70
+ "tests/test_service.py",
71
+ "def test_values(rows):\n assert (row.is_valid for row in rows)\n",
72
+ ),
73
+ ),
74
+ focus_path=PurePosixPath("tests/test_service.py"),
75
+ expected_count=1,
76
+ public=True,
77
+ ),
78
+ RuleExample(
79
+ example_id="consumed-generator",
80
+ scenario="deferred-condition",
81
+ title="Consume the conditions when the contract requires every row to be valid",
82
+ outcome=ExampleOutcome.NO_MATCH,
83
+ files=(
84
+ ExampleFile.python(
85
+ "tests/test_service.py",
86
+ "def test_values(rows):\n assert all(row.is_valid for row in rows)\n",
87
+ ),
88
+ ),
89
+ focus_path=PurePosixPath("tests/test_service.py"),
90
+ expected_count=0,
91
+ public=True,
92
+ ),
61
93
  RuleExample(
62
94
  example_id="setup-only-oracle",
63
95
  scenario="setup-only",
@@ -273,6 +305,10 @@ def _fixed_truth_reason(test: ast.expr) -> str | None:
273
305
  kind = _nonempty_container_kind(test)
274
306
  if kind is not None:
275
307
  return f"a non-empty {kind} display is truthy whatever it contains"
308
+ if isinstance(test, ast.GeneratorExp):
309
+ return "a generator expression creates a truthy iterator without checking its yielded values"
310
+ if isinstance(test, ast.Lambda):
311
+ return "a lambda expression creates a truthy function without calling its body"
276
312
  return None
277
313
 
278
314
 
@@ -218,15 +218,13 @@ def _is_syntax_required(owner: ast.Module | ast.ClassDef | ast.FunctionDef | ast
218
218
 
219
219
  def _owner_keys(tree: ast.Module) -> dict[int, str]:
220
220
  keys = {id(tree): "__doc__"}
221
-
222
- def visit(node: ast.AST, prefix: tuple[str, ...]) -> None:
221
+ pending: list[tuple[ast.AST, tuple[str, ...]]] = [(tree, ())]
222
+ while pending:
223
+ node, prefix = pending.pop()
223
224
  if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
224
225
  prefix = (*prefix, node.name)
225
226
  keys[id(node)] = ".".join(prefix)
226
- for child in ast.iter_child_nodes(node):
227
- visit(child, prefix)
228
-
229
- visit(tree, ())
227
+ pending.extend((child, prefix) for child in reversed(tuple(ast.iter_child_nodes(node))))
230
228
  return keys
231
229
 
232
230
 
@@ -90,7 +90,7 @@ class PreferClassRow(Rule):
90
90
  files=(
91
91
  ExampleFile.python(
92
92
  "app/task_store.py",
93
- "from psycopg.rows import class_row\n\nasync def load(conn):\n async with conn.cursor(row_factory=class_row(Task)) as cursor:\n return await cursor.fetchone()\n",
93
+ 'from psycopg.rows import class_row\n\nasync def load(conn):\n async with conn.cursor(row_factory=class_row(Task)) as cursor:\n await cursor.execute("SELECT id, state FROM task")\n return await cursor.fetchone()\n',
94
94
  ),
95
95
  ),
96
96
  focus_path=PurePosixPath("app/task_store.py"),