sarj-python-lint 1.10.0__tar.gz → 1.12.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 (169) hide show
  1. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_registry.py +4 -0
  4. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +120 -26
  5. sarj_python_lint-1.12.0/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +205 -0
  6. sarj_python_lint-1.12.0/src/sarj_python_lint/rules/require_precise_factory_signature.py +252 -0
  7. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/.gitignore +0 -0
  8. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/LICENSE +0 -0
  9. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/README.md +0 -0
  10. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/__init__.py +0 -0
  11. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/__main__.py +0 -0
  12. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  13. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/_file_context.py +0 -0
  14. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/_filesystem.py +0 -0
  15. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/_python_target.py +0 -0
  16. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  17. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/_secret_names.py +0 -0
  18. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/_version.py +0 -0
  19. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/benchmark.py +0 -0
  20. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/json_boundary.py +0 -0
  21. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/py.typed +0 -0
  22. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/ratchet.py +0 -0
  23. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rule_base.py +0 -0
  24. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  25. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  26. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  27. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  28. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  29. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  30. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  31. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  32. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  33. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  34. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  35. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  36. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  37. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  38. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  39. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  40. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  41. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  42. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  43. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  44. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  45. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  46. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  47. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  48. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  49. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  50. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  51. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  52. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/discourage_nullable_constructor_parameters.py +0 -0
  53. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  54. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  55. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  56. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  57. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  58. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  59. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  60. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  61. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  62. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  63. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  64. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  65. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  66. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  67. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  68. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  69. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  70. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  71. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  72. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  73. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  74. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  75. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  76. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  77. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  78. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  79. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  80. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  81. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  82. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  83. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  84. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  85. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  86. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  87. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  88. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  89. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  90. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  91. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  92. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  93. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  94. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  95. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  96. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  97. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  98. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  99. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  100. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  101. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  102. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  103. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  104. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  105. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  106. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  107. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  108. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  109. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  110. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  111. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  112. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  113. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  114. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  115. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  116. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  117. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  118. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  119. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  120. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  121. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  122. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  123. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  124. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  125. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  126. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  127. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  128. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  129. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  130. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  131. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  132. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  133. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  134. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  135. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  136. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  137. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  138. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  139. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  140. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  141. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  142. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  143. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  144. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  145. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  146. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  147. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  148. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  149. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  150. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  151. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  152. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  153. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  154. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  155. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  156. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  157. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  158. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  159. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  160. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  161. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  162. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  163. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  164. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  165. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  166. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  167. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  168. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  169. {sarj_python_lint-1.10.0 → sarj_python_lint-1.12.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.10.0
3
+ Version: 1.12.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.10.0"
3
+ version = "1.12.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" }]
@@ -79,6 +79,7 @@ from sarj_python_lint.rules.no_restated_comment import NoRestatedComment
79
79
  from sarj_python_lint.rules.no_secret_in_log import NoSecretInLog
80
80
  from sarj_python_lint.rules.no_select_star import NoSelectStar
81
81
  from sarj_python_lint.rules.no_service_behavior_in_settings import NoServiceBehaviorInSettings
82
+ from sarj_python_lint.rules.no_shallow_container_type_guard import NoShallowContainerTypeGuard
82
83
  from sarj_python_lint.rules.no_string_concat_in_loop import NoStringConcatInLoop
83
84
  from sarj_python_lint.rules.no_swallowed_asyncio_cancellation import NoSwallowedAsyncioCancellation
84
85
  from sarj_python_lint.rules.no_tautological_expect import NoTautologicalExpect
@@ -164,6 +165,7 @@ from sarj_python_lint.rules.require_nodecode_for_splitting_settings_field import
164
165
  RequireNoDecodeForSplittingSettingsField,
165
166
  )
166
167
  from sarj_python_lint.rules.require_port_for_service import RequirePortForService
168
+ from sarj_python_lint.rules.require_precise_factory_signature import RequirePreciseFactorySignature
167
169
  from sarj_python_lint.rules.require_public_dependency_contract import RequirePublicDependencyContract
168
170
  from sarj_python_lint.rules.require_pydantic_for_external_json import (
169
171
  RequirePydanticForExternalJson,
@@ -199,6 +201,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
199
201
  RequireExplicitContractImplementation.id: RequireExplicitContractImplementation,
200
202
  DiscourageNullableConstructorParameters.id: DiscourageNullableConstructorParameters,
201
203
  RequirePublicDependencyContract.id: RequirePublicDependencyContract,
204
+ RequirePreciseFactorySignature.id: RequirePreciseFactorySignature,
202
205
  RequireExplicitServiceContract.id: RequireExplicitServiceContract,
203
206
  AsyncCleanupRegisteredSynchronously.id: AsyncCleanupRegisteredSynchronously,
204
207
  SubprocessKillWithoutReap.id: SubprocessKillWithoutReap,
@@ -260,6 +263,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
260
263
  NoHiddenConstructorFallback.id: NoHiddenConstructorFallback,
261
264
  NoInjectedModuleLoader.id: NoInjectedModuleLoader,
262
265
  NoNullableDependencyFallback.id: NoNullableDependencyFallback,
266
+ NoShallowContainerTypeGuard.id: NoShallowContainerTypeGuard,
263
267
  NoFileLevelEscapeHatchSuppression.id: NoFileLevelEscapeHatchSuppression,
264
268
  NoInvalidArgumentNameSuppression.id: NoInvalidArgumentNameSuppression,
265
269
  NoFastapiOnEvent.id: NoFastapiOnEvent,
@@ -20,12 +20,23 @@ from sarj_python_lint.rules.no_hidden_constructor_fallback import LocalBindingCo
20
20
 
21
21
 
22
22
  if TYPE_CHECKING:
23
- from collections.abc import Iterator
23
+ from collections.abc import Iterator, Mapping
24
24
 
25
25
  from sarj_python_lint._file_context import PythonFileContext
26
26
 
27
27
 
28
28
  _LOADER_TARGETS = frozenset({"importlib.import_module", "importlib.__import__", "builtins.__import__"})
29
+ _LEXICAL_SCOPES = (
30
+ ast.Module,
31
+ ast.FunctionDef,
32
+ ast.AsyncFunctionDef,
33
+ ast.ClassDef,
34
+ ast.Lambda,
35
+ ast.ListComp,
36
+ ast.SetComp,
37
+ ast.DictComp,
38
+ ast.GeneratorExp,
39
+ )
29
40
 
30
41
 
31
42
  @final
@@ -42,7 +53,8 @@ class NoInjectedModuleLoader(Rule):
42
53
  limitations=(
43
54
  "Flags defaults resolving to importlib.import_module, importlib.__import__, or builtins.__import__, including stable module-level import and single-assignment aliases and unshadowed __import__.",
44
55
  "Tests, test support, and generated code are excluded. Required loader parameters, application loaders, wrapper lambdas, alias chains, comprehensions, and imports local to enclosing scopes are not inferred.",
45
- "Conservatively skips shadowed or rebound names, wildcard-import files, and files that reassign loader attributes. No autofix: removing an argument changes the callable contract and moving imports can change initialization timing.",
56
+ "Conservatively skips shadowed or rebound names, wildcard-import files, reassigned loader attributes, and exception or pattern captures of importer roots, symbols or assignment aliases. A relevant-name capture in an unrelated local scope can cause a false negative because capture checks are file-wide.",
57
+ "Assignment aliases require one module binding and precede use by line and column. Defaults use their enclosing evaluation scope: earlier statements in the nearest executing class body can shadow loaders; methods and nested classes do not close over class attributes. Enclosing function body bindings remain conservative exclusions, but a function's parameters do not shadow its own defaults. Relevant global declarations and walrus writes exclude sensitive importer names. Compound class statements can cause conservative omissions. Dynamic namespace mutation is not inferred. No autofix: changing arguments or import placement can alter API and initialization behavior.",
46
58
  ),
47
59
  examples=(
48
60
  RuleExample(
@@ -83,15 +95,21 @@ class NoInjectedModuleLoader(Rule):
83
95
  or context.generated
84
96
  or is_test_path(context.path)
85
97
  or is_test_support_path(context.path)
86
- or _ambiguous_loader_bindings(context)
87
98
  ):
88
99
  return []
89
100
  aliases = _stable_loader_aliases(context)
101
+ if _ambiguous_loader_bindings(context, aliases):
102
+ return []
90
103
  findings: list[Diagnostic] = []
91
104
  for function in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda):
92
105
  for argument, default in _defaults(function.args):
93
106
  direct = _is_loader(default, context)
94
- aliased = isinstance(default, ast.Name) and aliases.get(default.id, function.lineno) < function.lineno
107
+ aliased = (
108
+ isinstance(default, ast.Name)
109
+ and default.id in aliases
110
+ and (aliases[default.id].lineno, aliases[default.id].col_offset)
111
+ < (function.lineno, function.col_offset)
112
+ )
95
113
  if not (direct or aliased) or _shadowed(default, function, context):
96
114
  continue
97
115
  findings.append(
@@ -107,9 +125,28 @@ class NoInjectedModuleLoader(Rule):
107
125
  return sorted(findings, key=lambda finding: (finding.line, finding.col))
108
126
 
109
127
 
110
- def _ambiguous_loader_bindings(context: PythonFileContext) -> bool:
128
+ def _ambiguous_loader_bindings(context: PythonFileContext, aliases: Mapping[str, ast.stmt]) -> bool:
111
129
  if any(alias.name == "*" for node in context.nodes(ast.ImportFrom) for alias in node.names):
112
130
  return True
131
+ sensitive_names = {"__import__", *aliases} | {
132
+ name
133
+ for name, target in context.module_imports.bindings.items()
134
+ if (target.module in {"importlib", "builtins"} and target.symbol is None)
135
+ or f"{target.module}.{target.symbol}" in _LOADER_TARGETS
136
+ }
137
+ if any(sensitive_names.intersection(node.names) for node in context.nodes(ast.Global)):
138
+ return True
139
+ for node in context.nodes(ast.ExceptHandler, ast.MatchAs, ast.MatchStar, ast.MatchMapping, ast.NamedExpr):
140
+ match node:
141
+ case (
142
+ ast.ExceptHandler(name=name)
143
+ | ast.MatchAs(name=name)
144
+ | ast.MatchStar(name=name)
145
+ | ast.MatchMapping(rest=name)
146
+ | ast.NamedExpr(target=ast.Name(id=name))
147
+ ):
148
+ if name in sensitive_names:
149
+ return True
113
150
  return any(
114
151
  isinstance(node.ctx, (ast.Store, ast.Del))
115
152
  and context.module_imports.resolved_qualified_name(node) in _LOADER_TARGETS
@@ -144,27 +181,51 @@ def _shadowed(default: ast.expr, function: ast.AST, context: PythonFileContext)
144
181
  root = root.value
145
182
  if not isinstance(root, ast.Name):
146
183
  return True
147
- parent = context.parents.get(function)
184
+ child = function
185
+ parent = context.parents.get(child)
186
+ class_scope_visible = True
148
187
  while parent is not None:
149
188
  if isinstance(parent, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)):
150
189
  return True
151
190
  if isinstance(parent, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)):
152
- if root.id in scope_bindings(parent):
153
- return True
154
- elif isinstance(parent, ast.ClassDef):
155
- collector = LocalBindingCollector()
156
- for statement in parent.body:
157
- collector.visit(statement)
158
- if root.id in collector.names:
191
+ if _in_scope_body(parent, child):
192
+ if root.id in scope_bindings(parent):
193
+ return True
194
+ class_scope_visible = False
195
+ elif isinstance(parent, ast.ClassDef) and _in_scope_body(parent, child):
196
+ if class_scope_visible and _class_prefix_shadows(parent, child, root.id):
159
197
  return True
198
+ class_scope_visible = False
199
+ child = parent
160
200
  parent = context.parents.get(parent)
161
201
  return False
162
202
 
163
203
 
164
- def _stable_loader_aliases(context: PythonFileContext) -> dict[str, int]:
204
+ def _in_scope_body(scope: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Lambda, child: ast.AST) -> bool:
205
+ # Defaults and class headers execute outside the scope being defined.
206
+ return scope.body is child if isinstance(scope, ast.Lambda) else child in scope.body
207
+
208
+
209
+ def _class_prefix_shadows(owner: ast.ClassDef, containing: ast.AST, name: str) -> bool:
210
+ collector = LocalBindingCollector()
211
+ for statement in owner.body:
212
+ if statement is containing:
213
+ match statement:
214
+ case ast.Assign(value=value) | ast.AnnAssign(value=ast.expr() as value):
215
+ collector.visit(value)
216
+ case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef():
217
+ pass
218
+ case _:
219
+ collector.visit(statement)
220
+ break
221
+ collector.visit(statement)
222
+ return name in collector.names
223
+
224
+
225
+ def _stable_loader_aliases(context: PythonFileContext) -> dict[str, ast.stmt]:
165
226
  if context.tree is None:
166
227
  return {}
167
- aliases: dict[str, int] = {}
228
+ aliases: dict[str, ast.stmt] = {}
168
229
  for statement in context.tree.body:
169
230
  match statement:
170
231
  case (
@@ -173,16 +234,16 @@ def _stable_loader_aliases(context: PythonFileContext) -> dict[str, int]:
173
234
  ):
174
235
  if (
175
236
  _is_loader(value, context)
176
- and _loader_import_precedes(value, statement.lineno, context)
237
+ and _loader_import_precedes(value, statement, context)
177
238
  and _binding_count(name, context) == 1
178
239
  ):
179
- aliases[name] = statement.lineno
240
+ aliases[name] = statement
180
241
  case _:
181
242
  pass
182
243
  return aliases
183
244
 
184
245
 
185
- def _loader_import_precedes(value: ast.expr, line: int, context: PythonFileContext) -> bool:
246
+ def _loader_import_precedes(value: ast.expr, assignment: ast.stmt, context: PythonFileContext) -> bool:
186
247
  root = value
187
248
  while isinstance(root, ast.Attribute):
188
249
  root = root.value
@@ -191,7 +252,7 @@ def _loader_import_precedes(value: ast.expr, line: int, context: PythonFileConte
191
252
  if root.id == "__import__" and context.module_imports.builtin_is_unshadowed(root.id):
192
253
  return True
193
254
  return any(
194
- statement.lineno < line
255
+ (statement.lineno, statement.col_offset) < (assignment.lineno, assignment.col_offset)
195
256
  and any((alias.asname or alias.name.split(".")[0]) == root.id for alias in statement.names)
196
257
  for statement in context.tree.body
197
258
  if isinstance(statement, (ast.Import, ast.ImportFrom))
@@ -200,14 +261,47 @@ def _loader_import_precedes(value: ast.expr, line: int, context: PythonFileConte
200
261
 
201
262
  def _binding_count(name: str, context: PythonFileContext) -> int:
202
263
  return (
203
- sum(node.id == name and isinstance(node.ctx, (ast.Store, ast.Del)) for node in context.nodes(ast.Name))
204
- + sum(node.arg == name for node in context.nodes(ast.arg))
205
- + sum((node.asname or node.name.split(".")[0]) == name for node in context.nodes(ast.alias))
206
- + sum(
207
- node.name == name
264
+ sum(
265
+ _binds_name(node, name) and _is_module_binding(node, context)
208
266
  for node in context.nodes(
209
- ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.ExceptHandler, ast.MatchAs, ast.MatchStar
267
+ ast.Name,
268
+ ast.alias,
269
+ ast.FunctionDef,
270
+ ast.AsyncFunctionDef,
271
+ ast.ClassDef,
272
+ ast.ExceptHandler,
273
+ ast.MatchAs,
274
+ ast.MatchStar,
275
+ ast.MatchMapping,
210
276
  )
211
277
  )
212
- + sum(node.rest == name for node in context.nodes(ast.MatchMapping))
278
+ + sum(name in node.names for node in context.nodes(ast.Global))
279
+ + sum(node.target.id == name for node in context.nodes(ast.NamedExpr))
213
280
  )
281
+
282
+
283
+ def _binds_name(node: ast.AST, name: str) -> bool:
284
+ match node:
285
+ case (
286
+ ast.Name(id=bound, ctx=(ast.Store() | ast.Del()))
287
+ | ast.FunctionDef(name=bound)
288
+ | ast.AsyncFunctionDef(name=bound)
289
+ | ast.ClassDef(name=bound)
290
+ | ast.ExceptHandler(name=bound)
291
+ | ast.MatchAs(name=bound)
292
+ | ast.MatchStar(name=bound)
293
+ | ast.MatchMapping(rest=bound)
294
+ ):
295
+ return bound == name
296
+ case ast.alias(name=imported, asname=alias):
297
+ return (alias or imported.partition(".")[0]) == name
298
+ case _:
299
+ return False
300
+
301
+
302
+ def _is_module_binding(node: ast.AST, context: PythonFileContext) -> bool:
303
+ while node in context.parents:
304
+ node = context.parents[node]
305
+ if isinstance(node, _LEXICAL_SCOPES):
306
+ return isinstance(node, ast.Module)
307
+ return False
@@ -0,0 +1,205 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import PurePosixPath
5
+ from typing import TYPE_CHECKING, ClassVar, final, override
6
+
7
+ from sarj_python_lint.rule_base import (
8
+ AutofixPolicy,
9
+ Diagnostic,
10
+ ExampleFile,
11
+ ExampleOutcome,
12
+ Rule,
13
+ RuleCategory,
14
+ RuleDocumentation,
15
+ RuleExample,
16
+ Severity,
17
+ is_suppressed,
18
+ )
19
+ from sarj_python_lint.rules._imports import TYPING_SOURCES
20
+
21
+
22
+ if TYPE_CHECKING:
23
+ from sarj_python_lint._file_context import PythonFileContext
24
+ from sarj_python_lint.rules._imports import ImportIndex
25
+
26
+
27
+ _CONTAINERS = frozenset({"dict", "list", "set"})
28
+ _MAPPING_SLOTS = 2
29
+ _SLOTS = frozenset({"object", "str", "int", "bool", "float", "bytes"})
30
+ _BUILTINS = _SLOTS | _CONTAINERS | {"isinstance"}
31
+ _MUTATION_TARGETS = {f"builtins.{symbol}" for symbol in _BUILTINS} | {
32
+ "typing.TypeGuard",
33
+ "typing.TypeIs",
34
+ "typing_extensions.TypeGuard",
35
+ "typing_extensions.TypeIs",
36
+ }
37
+ _BAD = "from typing import TypeGuard\ndef is_mapping(value: object) -> TypeGuard[dict[str, object]]:\n return isinstance(value, dict)\n"
38
+ _GOOD = _BAD.replace("dict[str, object]", "dict[object, object]")
39
+
40
+
41
+ @final
42
+ class NoShallowContainerTypeGuard(Rule):
43
+ id = "no-shallow-container-type-guard"
44
+ code = "SARJ472"
45
+ documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
46
+ default_level=Severity.WARNING,
47
+ summary="Do not claim typed container contents from a container-only type guard.",
48
+ rationale="TypeGuard and TypeIs let callers trust their declared narrowing. Checking only dict, list, or set does not validate keys, values, or elements, so callers can perform operations that fail at runtime.",
49
+ remediation="Use object for unchecked container slots, or validate their contents when the narrower contract is required. Keep constrained input types that already prove the contents.",
50
+ category=RuleCategory.CORRECTNESS,
51
+ autofix=AutofixPolicy.NONE,
52
+ limitations=(
53
+ "Checks top-level, undecorated, non-generic synchronous functions with one positional parameter proven to be builtin object and a single return isinstance(parameter, builtin_container), optionally after a docstring.",
54
+ "The TypeGuard or TypeIs target must be a matching builtin dict, list, or set whose slots all resolve to object, str, int, bool, float, or bytes; at least one slot must be narrower than object.",
55
+ "Includes authored tests; generated and vendor source is excluded. String annotations, constrained input types, unions, Any, TypeVars, Protocols, assignment aliases, nested definitions, and content validators are not inferred.",
56
+ "Annotation imports include TYPE_CHECKING imports. Runtime imports use conservative file-wide shadow facts, so unrelated local rebinding can cause false negatives. Wildcard imports, explicit writes to relevant builtin or typing attributes, and exception, pattern or walrus bindings that shadow relevant names or imported aliases exclude the file; dynamic namespace mutation is not inferred.",
57
+ "No autofix: widening the return contract can require caller changes, while adding content validation changes behavior and cost.",
58
+ ),
59
+ examples=(
60
+ RuleExample(
61
+ example_id="unchecked-dictionary-keys",
62
+ title="A container check does not prove string keys",
63
+ outcome=ExampleOutcome.MATCH,
64
+ files=(ExampleFile.python("app/values.py", _BAD),),
65
+ focus_path=PurePosixPath("app/values.py"),
66
+ expected_count=1,
67
+ public=True,
68
+ ),
69
+ RuleExample(
70
+ example_id="honest-dictionary-keys",
71
+ title="Unchecked keys retain their object type",
72
+ outcome=ExampleOutcome.NO_MATCH,
73
+ files=(ExampleFile.python("app/values.py", _GOOD),),
74
+ focus_path=PurePosixPath("app/values.py"),
75
+ expected_count=0,
76
+ public=True,
77
+ ),
78
+ ),
79
+ )
80
+ description = documentation.summary
81
+
82
+ @override
83
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
84
+ if context.tree is None or context.generated or _ambiguous_symbols(context):
85
+ return []
86
+ findings: list[Diagnostic] = []
87
+ for function in context.tree.body:
88
+ if not isinstance(function, ast.FunctionDef):
89
+ continue
90
+ statement = _shallow_guard(function, context)
91
+ if statement is None or is_suppressed(context.source_lines, statement.lineno, self.code):
92
+ continue
93
+ findings.append(
94
+ Diagnostic(
95
+ path=context.path,
96
+ line=statement.lineno,
97
+ col=statement.col_offset + 1,
98
+ code=self.code,
99
+ message="Container-only isinstance check does not validate the declared container contents; use object for unchecked slots or validate the contents required by the return type.",
100
+ severity=Severity.WARNING,
101
+ )
102
+ )
103
+ return findings
104
+
105
+
106
+ def _shallow_guard(function: ast.FunctionDef, context: PythonFileContext) -> ast.Return | None:
107
+ arguments = function.args
108
+ positional = (*arguments.posonlyargs, *arguments.args)
109
+ if function.decorator_list or function.type_params or len(positional) != 1:
110
+ return None
111
+ if arguments.kwonlyargs or arguments.vararg is not None or arguments.kwarg is not None:
112
+ return None
113
+ parameter = positional[0]
114
+ if _builtin_symbol(parameter.annotation, context.module_imports) != "object":
115
+ return None
116
+ name = _guarded_container(function.returns, context.module_imports)
117
+ if name is None:
118
+ return None
119
+ body = function.body
120
+ if (
121
+ body
122
+ and isinstance(body[0], ast.Expr)
123
+ and isinstance(body[0].value, ast.Constant)
124
+ and isinstance(body[0].value.value, str)
125
+ ):
126
+ body = body[1:]
127
+ if len(body) != 1 or not isinstance(body[0], ast.Return):
128
+ return None
129
+ statement = body[0]
130
+ call = statement.value
131
+ if not isinstance(call, ast.Call) or call.keywords:
132
+ return None
133
+ match call.args:
134
+ case [ast.Name(id=guarded_name), checked_container] if (
135
+ guarded_name == parameter.arg
136
+ and _builtin_symbol(call.func, context.imports) == "isinstance"
137
+ and _builtin_symbol(checked_container, context.imports) == name
138
+ ):
139
+ return statement
140
+ case _:
141
+ return None
142
+
143
+
144
+ def _guarded_container(annotation: ast.expr | None, imports: ImportIndex) -> str | None:
145
+ if not isinstance(annotation, ast.Subscript) or imports.resolved_symbol(
146
+ annotation.value, sources=TYPING_SOURCES
147
+ ) not in {"TypeGuard", "TypeIs"}:
148
+ return None
149
+ container = annotation.slice
150
+ if not isinstance(container, ast.Subscript):
151
+ return None
152
+ name = _builtin_symbol(container.value, imports)
153
+ if name not in _CONTAINERS:
154
+ return None
155
+ slots = container.slice.elts if isinstance(container.slice, ast.Tuple) else [container.slice]
156
+ kinds = [_builtin_symbol(slot, imports) for slot in slots]
157
+ if (
158
+ len(kinds) != (_MAPPING_SLOTS if name == "dict" else 1)
159
+ or any(kind not in _SLOTS for kind in kinds)
160
+ or all(kind == "object" for kind in kinds)
161
+ ):
162
+ return None
163
+ return name
164
+
165
+
166
+ def _builtin_symbol(node: ast.expr | None, imports: ImportIndex) -> str | None:
167
+ if node is None:
168
+ return None
169
+ symbol = imports.resolved_symbol(node, sources=frozenset({"builtins"}))
170
+ if symbol in _BUILTINS:
171
+ return symbol
172
+ if isinstance(node, ast.Name) and node.id in _BUILTINS and imports.builtin_is_unshadowed(node.id):
173
+ return node.id
174
+ return None
175
+
176
+
177
+ def _ambiguous_symbols(context: PythonFileContext) -> bool:
178
+ sensitive_names = _BUILTINS | {
179
+ name
180
+ for name, target in context.module_imports.bindings.items()
181
+ if target.module in TYPING_SOURCES | {"builtins"}
182
+ and (target.symbol is None or target.symbol in _BUILTINS | {"TypeGuard", "TypeIs"})
183
+ }
184
+ for node in context.nodes(
185
+ ast.ImportFrom, ast.Attribute, ast.ExceptHandler, ast.MatchAs, ast.MatchStar, ast.MatchMapping, ast.NamedExpr
186
+ ):
187
+ match node:
188
+ case ast.ImportFrom(names=aliases):
189
+ if any(alias.name == "*" for alias in aliases):
190
+ return True
191
+ case ast.Attribute(ctx=(ast.Store() | ast.Del())):
192
+ if context.module_imports.resolved_qualified_name(node) in _MUTATION_TARGETS:
193
+ return True
194
+ case (
195
+ ast.ExceptHandler(name=name)
196
+ | ast.MatchAs(name=name)
197
+ | ast.MatchStar(name=name)
198
+ | ast.MatchMapping(rest=name)
199
+ | ast.NamedExpr(target=ast.Name(id=name))
200
+ ):
201
+ if name in sensitive_names:
202
+ return True
203
+ case _:
204
+ pass
205
+ return False