sarj-python-lint 1.49.0__tar.gz → 1.51.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 (223) hide show
  1. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/.gitignore +12 -1
  2. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/PKG-INFO +1 -1
  3. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/pyproject.toml +1 -1
  4. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_annotation_semantics.py +2 -1
  5. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_registry.py +2 -0
  6. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_resource_provenance.py +4 -3
  7. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +306 -27
  8. sarj_python_lint-1.51.0/src/sarj_python_lint/rules/no_mutating_container_transform.py +729 -0
  9. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/LICENSE +0 -0
  10. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/README.md +0 -0
  11. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/__init__.py +0 -0
  12. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/__main__.py +0 -0
  13. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  14. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/_file_context.py +0 -0
  15. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/_filesystem.py +0 -0
  16. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/_python_target.py +0 -0
  17. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  18. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/_secret_names.py +0 -0
  19. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/_source.py +0 -0
  20. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/_version.py +0 -0
  21. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/benchmark.py +0 -0
  22. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  23. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/json_boundary.py +0 -0
  24. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/py.typed +0 -0
  25. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/ratchet.py +0 -0
  26. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rule_base.py +0 -0
  27. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  28. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  29. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  30. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  31. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  32. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  33. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  34. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  35. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  36. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  37. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  38. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  39. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  40. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  41. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  42. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  43. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  44. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  45. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  46. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  47. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  48. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  49. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  50. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  51. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  52. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  53. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  54. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  55. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  56. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  57. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  58. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  59. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  60. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  61. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  62. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  63. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  64. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  65. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  66. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  67. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  68. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  69. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  70. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  71. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  72. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  73. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  74. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  75. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  76. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  77. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  78. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  79. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  80. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  81. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  82. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  83. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  84. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  85. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  86. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  87. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  88. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  89. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  90. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  91. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  92. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  93. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  94. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  95. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  96. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  97. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  98. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  99. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  100. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  101. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  102. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  103. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  104. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  105. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  106. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  107. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  108. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  109. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  110. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  111. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  112. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  113. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  114. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  115. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  116. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  117. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  118. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  119. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  120. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  121. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  122. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  123. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  124. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  125. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  126. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  127. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  128. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  129. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  130. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  131. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  132. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  133. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  134. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  135. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  136. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  137. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  138. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  139. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  140. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  141. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  142. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  143. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  144. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  145. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  146. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  147. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  148. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  149. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  150. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  151. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  152. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  153. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  154. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  155. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  156. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  157. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  158. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  159. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  160. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  161. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  162. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  163. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  164. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  165. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  166. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  167. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  168. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  169. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  170. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  171. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  172. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  173. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  174. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  175. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  176. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  177. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  178. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  179. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  180. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  181. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  182. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  183. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  184. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  185. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  186. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  187. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  188. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  189. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  190. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  191. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  192. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  193. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  194. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  195. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  196. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  197. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  198. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  199. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  200. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  201. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  202. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  203. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  204. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  205. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  206. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  207. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  208. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  209. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  210. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  211. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  212. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  213. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  214. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  215. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  216. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  217. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  218. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  219. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  220. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  221. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  222. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  223. {sarj_python_lint-1.49.0 → sarj_python_lint-1.51.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
@@ -32,7 +32,18 @@ Thumbs.db
32
32
  .zed/
33
33
  *.swp
34
34
 
35
- .claude/
35
+ .claude/*
36
+ !.claude/skills/
37
+ .claude/skills/*
38
+ !.claude/skills/audit-protocol/
39
+ .claude/skills/audit-protocol/*
40
+ !.claude/skills/audit-protocol/SKILL.md
41
+ !.claude/skills/lint-rule-generator/
42
+ .claude/skills/lint-rule-generator/*
43
+ !.claude/skills/lint-rule-generator/SKILL.md
44
+ !.claude/skills/test-value/
45
+ .claude/skills/test-value/*
46
+ !.claude/skills/test-value/SKILL.md
36
47
  apps/docs/.wrangler/
37
48
  apps/docs/.dev.vars*
38
49
  !apps/docs/.dev.vars.example
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.49.0
3
+ Version: 1.51.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 = "1.49.0"
3
+ version = "1.51.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" }]
@@ -47,7 +47,8 @@ class AnnotationSemantics:
47
47
  aliases = {name: value for name, value in candidates.items() if name not in ambiguous and name not in rebound}
48
48
  return cls(imports, MappingProxyType(aliases))
49
49
 
50
- def parse(self, annotation: ast.expr | None) -> ast.expr | None:
50
+ @staticmethod
51
+ def parse(annotation: ast.expr | None) -> ast.expr | None:
51
52
  if not isinstance(annotation, ast.Constant) or not isinstance(annotation.value, str):
52
53
  return annotation
53
54
  try:
@@ -70,6 +70,7 @@ from sarj_python_lint.rules.no_invalid_argument_name_suppression import NoInvali
70
70
  from sarj_python_lint.rules.no_loguru_exc_info import NoLoguruExcInfo
71
71
  from sarj_python_lint.rules.no_loguru_printf_format import NoLoguruPrintfFormat
72
72
  from sarj_python_lint.rules.no_mocked_pydantic_value_object import NoMockedPydanticValueObject
73
+ from sarj_python_lint.rules.no_mutating_container_transform import NoMutatingContainerTransform
73
74
  from sarj_python_lint.rules.no_nested_pydantic_field_validator import NoNestedPydanticFieldValidator
74
75
  from sarj_python_lint.rules.no_nullable_dependency_fallback import NoNullableDependencyFallback
75
76
  from sarj_python_lint.rules.no_offset_pagination import NoOffsetPagination
@@ -251,6 +252,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
251
252
  NoInterpreterSourceArguments.id: NoInterpreterSourceArguments,
252
253
  NoUnusedCopyResult.id: NoUnusedCopyResult,
253
254
  NoDiscardedValidationResult.id: NoDiscardedValidationResult,
255
+ NoMutatingContainerTransform.id: NoMutatingContainerTransform,
254
256
  NoBeforeValidatorInputMutation.id: NoBeforeValidatorInputMutation,
255
257
  NoInputModelMutation.id: NoInputModelMutation,
256
258
  NoDatabaseProgrammability.id: NoDatabaseProgrammability,
@@ -34,8 +34,9 @@ _RESOURCE_OWNERS = MappingProxyType(
34
34
  "contextlib": frozenset({"ExitStack", "AsyncExitStack", "_BaseExitStack"}),
35
35
  "subprocess": frozenset({"Popen", "TimeoutExpired"}),
36
36
  "re": frozenset({"compile", "Match", "Pattern"}),
37
+ "operator": frozenset({"setitem", "delitem", "iadd", "iconcat", "imul", "iand", "isub", "ixor", "ior"}),
37
38
  "unittest": frozenset({"mock.Mock", "mock.MagicMock", "mock.AsyncMock"}),
38
- "typing": frozenset({"final", "Literal", "Annotated"}),
39
+ "typing": frozenset({"final", "Literal", "Annotated", "cast", "assert_type"}),
39
40
  "typing_extensions": frozenset({"final", "Literal", "Annotated"}),
40
41
  "pydantic": frozenset(
41
42
  {
@@ -154,7 +155,7 @@ class ResourceProvenance:
154
155
  binding = self.runtime_binding(root, node)
155
156
  if not isinstance(binding, ast.alias):
156
157
  return None
157
- qualified = _runtime_import_name(node, binding, self.context.parents.get(binding))
158
+ qualified = import_name_for_binding(node, binding, self.context.parents.get(binding))
158
159
  if qualified is None or not self._owner_namespace_untouched(qualified.partition(".")[0]):
159
160
  return None
160
161
  if binding not in self.runtime_import_cache:
@@ -528,7 +529,7 @@ def _root_name(node: ast.expr) -> str | None:
528
529
  return node.id if isinstance(node, ast.Name) else None
529
530
 
530
531
 
531
- def _runtime_import_name(expression: ast.expr, binding: ast.alias, parent: ast.AST | None) -> str | None:
532
+ def import_name_for_binding(expression: ast.expr, binding: ast.alias, parent: ast.AST | None) -> str | None:
532
533
  attributes: list[str] = []
533
534
  while isinstance(expression, ast.Attribute):
534
535
  attributes.append(expression.attr)
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import ast
4
4
  from dataclasses import dataclass
5
+ from enum import Enum, auto
5
6
  from functools import cached_property
6
7
  from pathlib import PurePosixPath
7
8
  from typing import TYPE_CHECKING, ClassVar, final, override
@@ -18,7 +19,8 @@ from sarj_python_lint.rule_base import (
18
19
  Severity,
19
20
  is_suppressed,
20
21
  )
21
- from sarj_python_lint.rules._resource_provenance import ResourceProvenance
22
+ from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics
23
+ from sarj_python_lint.rules._resource_provenance import ResourceProvenance, import_name_for_binding
22
24
 
23
25
 
24
26
  if TYPE_CHECKING:
@@ -30,6 +32,81 @@ if TYPE_CHECKING:
30
32
  type _Function = ast.FunctionDef | ast.AsyncFunctionDef
31
33
  _MAX_REEXPORTS = 8
32
34
  _MAX_ALIASES = 8
35
+ _MAX_ANNOTATION_DEPTH = 32
36
+ _VALUE_BUILTINS = frozenset(
37
+ {
38
+ "bool",
39
+ "bytes",
40
+ "bytearray",
41
+ "complex",
42
+ "dict",
43
+ "float",
44
+ "frozenset",
45
+ "int",
46
+ "list",
47
+ "memoryview",
48
+ "range",
49
+ "set",
50
+ "str",
51
+ "tuple",
52
+ "type",
53
+ }
54
+ )
55
+ _VALUE_TYPING = frozenset(
56
+ {
57
+ "AbstractSet",
58
+ "AsyncContextManager",
59
+ "AsyncGenerator",
60
+ "AsyncIterable",
61
+ "AsyncIterator",
62
+ "Awaitable",
63
+ "Callable",
64
+ "Collection",
65
+ "Container",
66
+ "ContextManager",
67
+ "Coroutine",
68
+ "Counter",
69
+ "DefaultDict",
70
+ "Deque",
71
+ "Dict",
72
+ "FrozenSet",
73
+ "Generator",
74
+ "Iterable",
75
+ "Iterator",
76
+ "List",
77
+ "Mapping",
78
+ "Match",
79
+ "MutableMapping",
80
+ "MutableSequence",
81
+ "MutableSet",
82
+ "Pattern",
83
+ "Sequence",
84
+ "Set",
85
+ "Sized",
86
+ "Tuple",
87
+ "Type",
88
+ "TypeGuard",
89
+ "TypeIs",
90
+ }
91
+ )
92
+ _NOMINAL_BASES = frozenset(
93
+ {
94
+ "abc.ABC",
95
+ "enum.Enum",
96
+ "enum.Flag",
97
+ "enum.IntEnum",
98
+ "enum.IntFlag",
99
+ "enum.StrEnum",
100
+ "pydantic.BaseModel",
101
+ "pydantic.main.BaseModel",
102
+ "typing.Generic",
103
+ "typing.NamedTuple",
104
+ "typing.TypedDict",
105
+ "typing_extensions.Generic",
106
+ "typing_extensions.NamedTuple",
107
+ "typing_extensions.TypedDict",
108
+ }
109
+ )
33
110
  _TEST_CASES = frozenset(
34
111
  {
35
112
  "unittest.TestCase",
@@ -41,6 +118,10 @@ _TEST_CASES = frozenset(
41
118
  _EXCEPTION_ASSERTIONS = frozenset({"assertRaises", "assertRaisesRegex"})
42
119
 
43
120
 
121
+ class _UnboundTypeName(Enum):
122
+ NAME = auto()
123
+
124
+
44
125
  @dataclass(frozen=True, slots=True)
45
126
  class _AssertionWrites:
46
127
  bindings: frozenset[tuple[ast.AST, str]]
@@ -75,7 +156,8 @@ class NoDiscardedValidationResult(Rule):
75
156
  "Direct execution in class bodies is excluded because sequential lookup and metaclass-prepared namespaces can supply different callees. Method bodies remain checked.",
76
157
  "Functions with explicit mutation, standalone command calls, generators, nested definitions, or context-manager/try/loop orchestration are excluded. This is not a general purity proof.",
77
158
  "Import-resolved pytest.raises and source-proven unittest exception assertion blocks observe the exception contract and are excluded. Generated and vendored sources are excluded; maintained tests are checked.",
78
- "Optional, Any, Never, and NoReturn return contracts are excluded. No automatic assignment, deletion, or validation rewrite is offered.",
159
+ "Top-level return contracts follow stable lexical aliases, first-party type imports, unions, and Annotated type content through a bounded source-only analysis. Nullable collection elements and annotation metadata do not make the returned collection nullable.",
160
+ "Nullable, object, Any, Never, NoReturn, generic type-parameter, structural protocol, and unresolved return contracts are excluded. No automatic assignment, deletion, or validation rewrite is offered.",
79
161
  ),
80
162
  examples=(
81
163
  RuleExample(
@@ -146,6 +228,7 @@ class _ValidationResults:
146
228
  self.provenance = ResourceProvenance(context, require_static_namespace=True)
147
229
  self.owners: dict[Path, tuple[PythonFileContext, ResourceProvenance]] = {}
148
230
  self.contracts: dict[_Function, bool] = {}
231
+ self.annotations: dict[ast.Module, AnnotationSemantics] = {}
149
232
 
150
233
  def discarded(self, statement: ast.Expr, call: ast.Call) -> bool:
151
234
  if self.provenance.dynamic_namespace or self.provenance.wildcard or self._expected_exception(statement):
@@ -157,7 +240,13 @@ class _ValidationResults:
157
240
  if isinstance(function, ast.AsyncFunctionDef) != isinstance(statement.value, ast.Await):
158
241
  return False
159
242
  if function not in self.contracts:
160
- self.contracts[function] = not owner.generated and _value_validator(function)
243
+ provenance = self._source_provenance(owner.path)
244
+ self.contracts[function] = (
245
+ not owner.generated
246
+ and _value_validator(function)
247
+ and provenance is not None
248
+ and self._value_annotation(provenance, function.returns, function, _MAX_ANNOTATION_DEPTH, frozenset())
249
+ )
161
250
  return self.contracts[function] and not any(
162
251
  _namespace_uncertain(provenance, owner.path) or _namespace_uncertain(provenance, provenance.context.path)
163
252
  for provenance in resolved.namespaces
@@ -238,8 +327,6 @@ class _ValidationResults:
238
327
  return (path, name) if path is not None else None
239
328
 
240
329
  def _imported_function(self, location: tuple[Path, str]) -> _ResolvedValidation | None:
241
- from sarj_python_lint._file_context import PythonFileContext # ruff: ignore[import-outside-top-level] -- avoid the registry and analysis-session import cycle.
242
-
243
330
  seen: set[tuple[Path, str]] = set()
244
331
  namespaces = [self.provenance]
245
332
  for _ in range(_MAX_REEXPORTS):
@@ -247,13 +334,10 @@ class _ValidationResults:
247
334
  return None
248
335
  seen.add(location)
249
336
  path, name = location
250
- module = self.context.session.local_source.read_module(path)
251
- if module is None:
337
+ provenance = self._source_provenance(path)
338
+ if provenance is None:
252
339
  return None
253
- if path not in self.owners:
254
- owner = PythonFileContext(path, module.source, self.context.session)
255
- self.owners[path] = owner, ResourceProvenance(owner, require_static_namespace=True)
256
- owner, provenance = self.owners[path]
340
+ owner = provenance.context
257
341
  if provenance.dynamic_namespace or provenance.wildcard or name in provenance.mutated:
258
342
  return None
259
343
  namespaces.append(provenance)
@@ -273,6 +357,187 @@ class _ValidationResults:
273
357
  location = next_location
274
358
  return None
275
359
 
360
+ def _source_provenance(self, path: Path) -> ResourceProvenance | None:
361
+ from sarj_python_lint._file_context import PythonFileContext # ruff: ignore[import-outside-top-level] -- avoid the registry and analysis-session import cycle.
362
+
363
+ if path == self.context.path:
364
+ return self.provenance
365
+ if path not in self.owners:
366
+ module = self.context.session.local_source.read_module(path)
367
+ if module is None:
368
+ return None
369
+ owner = PythonFileContext(path, module.source, self.context.session)
370
+ self.owners[path] = owner, ResourceProvenance(owner, require_static_namespace=True)
371
+ return self.owners[path][1]
372
+
373
+ def _value_annotation(
374
+ self,
375
+ provenance: ResourceProvenance,
376
+ annotation: ast.expr | None,
377
+ at: ast.AST,
378
+ remaining: int,
379
+ seen: frozenset[ast.AST],
380
+ ) -> bool:
381
+ if remaining <= 0 or provenance.context.tree is None or provenance.wildcard:
382
+ return False
383
+ tree = provenance.context.tree
384
+ if tree not in self.annotations:
385
+ self.annotations[tree] = AnnotationSemantics.from_tree(tree)
386
+ annotation = self.annotations[tree].parse(annotation)
387
+ match annotation:
388
+ case ast.BinOp(left=left, op=ast.BitOr(), right=right):
389
+ return self._value_annotation(provenance, left, at, remaining - 1, seen) and self._value_annotation(
390
+ provenance, right, at, remaining - 1, seen
391
+ )
392
+ case ast.Subscript():
393
+ return self._subscript_annotation(provenance, annotation, at, remaining - 1, seen)
394
+ case ast.Name() | ast.Attribute():
395
+ return self._annotation_atom(provenance, annotation, at, remaining - 1, seen)
396
+ case _:
397
+ return False
398
+
399
+ def _subscript_annotation(
400
+ self,
401
+ provenance: ResourceProvenance,
402
+ annotation: ast.Subscript,
403
+ at: ast.AST,
404
+ remaining: int,
405
+ seen: frozenset[ast.AST],
406
+ ) -> bool:
407
+ qualified = self._annotation_import(provenance, annotation.value, at)
408
+ content = annotation.slice
409
+ arguments = content.elts if isinstance(content, ast.Tuple) else [content]
410
+ match qualified:
411
+ case "typing.Annotated" | "typing_extensions.Annotated":
412
+ return bool(arguments) and self._value_annotation(provenance, arguments[0], at, remaining, seen)
413
+ case "typing.Union" | "typing_extensions.Union":
414
+ return bool(arguments) and all(
415
+ self._value_annotation(provenance, item, at, remaining, seen) for item in arguments
416
+ )
417
+ case "typing.Literal" | "typing_extensions.Literal":
418
+ return bool(arguments) and all(
419
+ isinstance(item, ast.Constant) and item.value is not None for item in arguments
420
+ )
421
+ case _:
422
+ return self._value_annotation(provenance, annotation.value, at, remaining, seen)
423
+
424
+ def _annotation_atom(
425
+ self,
426
+ provenance: ResourceProvenance,
427
+ annotation: ast.Name | ast.Attribute,
428
+ at: ast.AST,
429
+ remaining: int,
430
+ seen: frozenset[ast.AST],
431
+ ) -> bool:
432
+ root = annotation
433
+ attributes: list[str] = []
434
+ while isinstance(root, ast.Attribute):
435
+ attributes.append(root.attr)
436
+ root = root.value
437
+ if not isinstance(root, ast.Name):
438
+ return False
439
+ binding = _annotation_binding(provenance, root.id, at)
440
+ if binding is _UnboundTypeName.NAME:
441
+ return not attributes and root.id in _VALUE_BUILTINS and provenance.builtin_namespace_untouched
442
+ if binding is None or binding in seen:
443
+ return False
444
+ seen |= {binding}
445
+ if isinstance(binding, ast.alias):
446
+ return self._imported_annotation(
447
+ provenance, annotation, binding, attributes, remaining=remaining, seen=seen
448
+ )
449
+ if attributes:
450
+ return False
451
+ if isinstance(binding, ast.ClassDef):
452
+ return all(self._nominal_base(provenance, base, binding, remaining, seen) for base in binding.bases)
453
+ parent = provenance.context.parents.get(binding)
454
+ match parent:
455
+ case ast.TypeAlias(value=value):
456
+ return self._value_annotation(provenance, value, parent, remaining - 1, seen)
457
+ case ast.Assign(targets=[target], value=value) if target is binding:
458
+ return self._value_annotation(provenance, value, parent, remaining - 1, seen)
459
+ case ast.AnnAssign(annotation=declaration, value=value) if self._annotation_import(
460
+ provenance, declaration, parent
461
+ ) in {"typing.TypeAlias", "typing_extensions.TypeAlias"}:
462
+ return self._value_annotation(provenance, value, parent, remaining - 1, seen)
463
+ case _:
464
+ return False
465
+
466
+ def _imported_annotation(
467
+ self,
468
+ provenance: ResourceProvenance,
469
+ annotation: ast.expr,
470
+ binding: ast.alias,
471
+ attributes: list[str],
472
+ *,
473
+ remaining: int,
474
+ seen: frozenset[ast.AST],
475
+ ) -> bool:
476
+ qualified = import_name_for_binding(annotation, binding, provenance.context.parents.get(binding))
477
+ if qualified is not None and qualified.partition(".")[0] in {"typing", "typing_extensions"}:
478
+ return qualified.rpartition(".")[2] in _VALUE_TYPING
479
+ if qualified is not None and qualified.startswith("builtins."):
480
+ return qualified.rpartition(".")[2] in _VALUE_BUILTINS and provenance.builtin_namespace_untouched
481
+ location = self._import_location(provenance.context, binding, tuple(reversed(attributes)))
482
+ if location is None or remaining <= 0:
483
+ return False
484
+ path, name = location
485
+ provider = self._source_provenance(path)
486
+ return (
487
+ provider is not None
488
+ and provider.context.tree is not None
489
+ and self._value_annotation(
490
+ provider, ast.Name(id=name, ctx=ast.Load()), provider.context.tree, remaining - 1, seen
491
+ )
492
+ )
493
+
494
+ def _annotation_import(self, provenance: ResourceProvenance, annotation: ast.expr, at: ast.AST) -> str | None:
495
+ root = annotation
496
+ while isinstance(root, ast.Attribute):
497
+ root = root.value
498
+ if not isinstance(root, ast.Name):
499
+ return None
500
+ binding = _annotation_binding(provenance, root.id, at)
501
+ return (
502
+ import_name_for_binding(annotation, binding, provenance.context.parents.get(binding))
503
+ if isinstance(binding, ast.alias)
504
+ else None
505
+ )
506
+
507
+ def _nominal_base(
508
+ self, provenance: ResourceProvenance, base: ast.expr, at: ast.AST, remaining: int, seen: frozenset[ast.AST]
509
+ ) -> bool:
510
+ if remaining <= 0:
511
+ return False
512
+ if isinstance(base, ast.Subscript):
513
+ base = base.value
514
+ qualified = self._annotation_import(provenance, base, at)
515
+ if qualified in _NOMINAL_BASES:
516
+ return True
517
+ if isinstance(base, ast.Name):
518
+ binding = _annotation_binding(provenance, base.id, at)
519
+ if isinstance(binding, ast.ClassDef):
520
+ return True
521
+ if binding is _UnboundTypeName.NAME:
522
+ return base.id in (_VALUE_BUILTINS | {"object"}) and provenance.builtin_namespace_untouched
523
+ if isinstance(binding, ast.alias) and binding not in seen:
524
+ location = self._import_location(provenance.context, binding)
525
+ if location is not None:
526
+ path, name = location
527
+ provider = self._source_provenance(path)
528
+ return (
529
+ provider is not None
530
+ and provider.context.tree is not None
531
+ and self._nominal_base(
532
+ provider,
533
+ ast.Name(id=name, ctx=ast.Load()),
534
+ provider.context.tree,
535
+ remaining - 1,
536
+ seen | {binding},
537
+ )
538
+ )
539
+ return False
540
+
276
541
  def _expected_exception(self, node: ast.AST) -> bool:
277
542
  parent = self.context.parents.get(node)
278
543
  while parent is not None and not isinstance(parent, ast.FunctionDef | ast.AsyncFunctionDef):
@@ -384,7 +649,7 @@ def _namespace_uncertain(provenance: ResourceProvenance, path: Path) -> bool:
384
649
 
385
650
 
386
651
  def _value_validator(function: _Function) -> bool:
387
- if function.decorator_list or not _value_annotation(function.returns):
652
+ if function.decorator_list or function.returns is None:
388
653
  return False
389
654
  statements = list(function.body)
390
655
  raised = returned = False
@@ -407,21 +672,35 @@ def _value_validator(function: _Function) -> bool:
407
672
  return raised and returned
408
673
 
409
674
 
410
- def _value_annotation(annotation: ast.expr | None) -> bool:
411
- if annotation is None:
412
- return False
413
- if isinstance(annotation, ast.Constant) and isinstance(annotation.value, str):
414
- try:
415
- annotation = ast.parse(annotation.value, mode="eval").body
416
- except SyntaxError:
417
- return False
418
- excluded = {"Any", "Never", "NoReturn", "Optional"}
419
- return not any(
420
- (isinstance(node, ast.Constant) and node.value is None)
421
- or (isinstance(node, ast.Name) and node.id in excluded)
422
- or (isinstance(node, ast.Attribute) and node.attr in excluded)
423
- for node in ast.walk(annotation)
424
- )
675
+ def _annotation_binding(provenance: ResourceProvenance, name: str, at: ast.AST) -> ast.AST | _UnboundTypeName | None:
676
+ if name in provenance.external_bindings:
677
+ return None
678
+ parameter = _annotation_parameter(at, name)
679
+ if parameter is not None:
680
+ return parameter
681
+ scope = at if isinstance(at, ast.Module) else provenance.scope(at)
682
+ first = True
683
+ while scope is not None:
684
+ if isinstance(scope, ast.ClassDef) and not first:
685
+ scope = provenance.scope(scope)
686
+ continue
687
+ parameter = _annotation_parameter(scope, name)
688
+ if parameter is not None:
689
+ return parameter
690
+ bindings = provenance.bindings.get(scope, {}).get(name, ())
691
+ if bindings:
692
+ return bindings[0] if len(bindings) == 1 else None
693
+ first = False
694
+ scope = provenance.scope(scope)
695
+ return _UnboundTypeName.NAME
696
+
697
+
698
+ def _annotation_parameter(at: ast.AST, name: str) -> ast.AST | None:
699
+ if isinstance(at, ast.FunctionDef | ast.AsyncFunctionDef | ast.TypeAlias | ast.ClassDef):
700
+ for parameter in at.type_params:
701
+ if isinstance(parameter, ast.TypeVar | ast.ParamSpec | ast.TypeVarTuple) and parameter.name == name:
702
+ return parameter
703
+ return None
425
704
 
426
705
 
427
706
  def _expression_effect(value: ast.expr, *, allow_calls: bool = False) -> bool: