sarj-python-lint 0.84.3__tar.gz → 0.85.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 (140) hide show
  1. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/__main__.py +6 -2
  4. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_fastapi.py +3 -0
  5. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_imports.py +6 -0
  6. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_registry.py +6 -4
  7. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +27 -4
  8. sarj_python_lint-0.84.3/src/sarj_python_lint/rules/pydantic_at_boundaries.py → sarj_python_lint-0.85.0/src/sarj_python_lint/rules/named_record_at_boundaries.py +8 -9
  9. sarj_python_lint-0.85.0/src/sarj_python_lint/rules/no_any_mapping_types.py +257 -0
  10. sarj_python_lint-0.85.0/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +420 -0
  11. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +59 -8
  12. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +65 -3
  13. sarj_python_lint-0.84.3/src/sarj_python_lint/rules/no_vague_annotations.py +0 -173
  14. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/.gitignore +0 -0
  15. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/LICENSE +0 -0
  16. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/README.md +0 -0
  17. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/__init__.py +0 -0
  18. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  19. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/_filesystem.py +0 -0
  20. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/_python_target.py +0 -0
  21. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  22. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/_secret_names.py +0 -0
  23. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/_version.py +0 -0
  24. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/py.typed +0 -0
  25. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/ratchet.py +0 -0
  26. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rule_base.py +0 -0
  27. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  28. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  29. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  30. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  31. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  32. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  33. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  34. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  35. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  36. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  37. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  38. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  39. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  40. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  41. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  42. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  43. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  44. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  45. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  46. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  47. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  48. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  49. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  50. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  51. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  52. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  53. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  54. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  55. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  56. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  57. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  58. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  59. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  60. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  61. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  62. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  63. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  64. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  65. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  66. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  67. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  68. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  69. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  70. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  71. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  72. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  73. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  74. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  75. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  76. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  77. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  78. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  79. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  80. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  81. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  82. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  83. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  84. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  85. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  86. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  87. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  88. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  89. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  90. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  91. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  92. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  93. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  94. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  95. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  96. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  97. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  98. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  99. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  100. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  101. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  102. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  103. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  104. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  105. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  106. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  107. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  108. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  109. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  110. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  111. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  112. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  113. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  114. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  115. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  116. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  117. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  118. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  119. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  120. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  121. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  122. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  123. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  124. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  125. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  126. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  127. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  128. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  129. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  130. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/require_typed_http_test_response.py +0 -0
  131. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  132. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  133. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  134. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  135. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  136. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  137. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  138. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  139. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  140. {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 0.84.3
3
+ Version: 0.85.0
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
6
6
  Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "0.84.3"
3
+ version = "0.85.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" }]
@@ -143,6 +143,10 @@ _DIAGNOSTIC_PRECEDENCE = MappingProxyType(
143
143
  "SARJ092": frozenset({"SARJ086", "SARJ087", "SARJ420"}),
144
144
  "SARJ093": frozenset({"SARJ034"}),
145
145
  "SARJ099": frozenset({"SARJ420"}),
146
+ # Any-valued mappings are the stronger correctness failure. SARJ008
147
+ # still owns every fixed dictionary return when selected alone, while
148
+ # an all-rules run emits one actionable diagnostic for this overlap.
149
+ "SARJ447": frozenset({"SARJ008"}),
146
150
  }
147
151
  )
148
152
 
@@ -162,13 +166,13 @@ def deduplicate_diagnostics(diags: list[Diagnostic], *, source: str | None = Non
162
166
  "SARJ420" in codes and not codes.isdisjoint(_DOCSTRING_PRECEDENCE_CODES)
163
167
  )
164
168
  docstring_owners = _docstring_owner_locations(source) if source is not None and needs_docstring_owners else {}
165
- needs_signature_owners = "SARJ093" in codes and "SARJ034" in codes
169
+ needs_signature_owners = ("SARJ093" in codes and "SARJ034" in codes) or ("SARJ447" in codes and "SARJ008" in codes)
166
170
  signature_owners = (
167
171
  _function_signature_owner_locations(source) if source is not None and needs_signature_owners else {}
168
172
  )
169
173
 
170
174
  def owner_location(diagnostic: Diagnostic) -> _OwnerLocation:
171
- if diagnostic.code in {"SARJ034", "SARJ093"}:
175
+ if diagnostic.code in {"SARJ008", "SARJ034", "SARJ093", "SARJ447"}:
172
176
  line, column = signature_owners.get(diagnostic.line, (diagnostic.line, diagnostic.col))
173
177
  else:
174
178
  line, column = docstring_owners.get(diagnostic.line, (diagnostic.line, diagnostic.col))
@@ -5,6 +5,8 @@ from collections import Counter
5
5
  from dataclasses import dataclass
6
6
  from typing import TYPE_CHECKING, Literal, NamedTuple
7
7
 
8
+ from sarj_python_lint.rules._imports import ImportIndex
9
+
8
10
 
9
11
  if TYPE_CHECKING:
10
12
  from pathlib import Path
@@ -107,6 +109,7 @@ class FastapiIndex:
107
109
  def __init__(self, tree: ast.Module, *, path: Path | None = None) -> None:
108
110
  self.tree: ast.Module = tree
109
111
  self.path: Path | None = path
112
+ self.imports: ImportIndex = ImportIndex.from_tree(tree, module_scope_only=True)
110
113
  self._imported_dependency_cache: dict[_ImportedReference, bool] = {}
111
114
  self._imported_module_cache: dict[Path, ast.Module | None] = {}
112
115
  self.modules: set[str] = set()
@@ -44,6 +44,12 @@ class ImportIndex:
44
44
  return None
45
45
  return resolved.symbol
46
46
 
47
+ def resolved_qualified_name(self, node: ast.expr) -> str | None:
48
+ resolved = self._resolve(node)
49
+ if resolved is None or resolved.symbol is None:
50
+ return None
51
+ return f"{resolved.module}.{resolved.symbol}"
52
+
47
53
  def builtin_is_unshadowed(self, name: str) -> bool:
48
54
  return name not in self.shadowed_names and name not in self.bindings
49
55
 
@@ -22,12 +22,14 @@ from sarj_python_lint.rules.invalid_pydantic_field_default import (
22
22
  InvalidPydanticFieldDefault,
23
23
  )
24
24
  from sarj_python_lint.rules.mock_without_spec import MockWithoutSpec
25
+ from sarj_python_lint.rules.named_record_at_boundaries import NamedRecordAtBoundaries
25
26
  from sarj_python_lint.rules.negative_only_http_status_assertion import (
26
27
  NegativeOnlyHttpStatusAssertion,
27
28
  )
28
29
  from sarj_python_lint.rules.no_analytical_aggregation_in_postgres_store import (
29
30
  NoAnalyticalAggregationInPostgresStore,
30
31
  )
32
+ from sarj_python_lint.rules.no_any_mapping_types import NoAnyMappingTypes
31
33
  from sarj_python_lint.rules.no_comment_cruft import NoCommentCruft
32
34
  from sarj_python_lint.rules.no_conftest_test_module_import import NoConftestTestModuleImport
33
35
  from sarj_python_lint.rules.no_copied_inherited_docstring import NoCopiedInheritedDocstring
@@ -78,7 +80,6 @@ from sarj_python_lint.rules.no_unique_violation_message_match import (
78
80
  )
79
81
  from sarj_python_lint.rules.no_unnecessary_docstring import NoUnnecessaryDocstring
80
82
  from sarj_python_lint.rules.no_unused_value_marker import NoUnusedValueMarker
81
- from sarj_python_lint.rules.no_vague_annotations import NoVagueAnnotations
82
83
  from sarj_python_lint.rules.no_vague_suppression_description import (
83
84
  NoVagueSuppressionDescription,
84
85
  )
@@ -102,6 +103,7 @@ from sarj_python_lint.rules.prefer_injected_dependency_over_monkeypatch import (
102
103
  )
103
104
  from sarj_python_lint.rules.prefer_library_fake import PreferLibraryFake
104
105
  from sarj_python_lint.rules.prefer_match_assert_never import PreferMatchAssertNever
106
+ from sarj_python_lint.rules.prefer_match_exception_dispatch import PreferMatchExceptionDispatch
105
107
  from sarj_python_lint.rules.prefer_match_type_dispatch import PreferMatchTypeDispatch
106
108
  from sarj_python_lint.rules.prefer_match_value_dispatch import PreferMatchValueDispatch
107
109
  from sarj_python_lint.rules.prefer_module_level_constant import (
@@ -134,7 +136,6 @@ from sarj_python_lint.rules.prefer_walrus_comprehension_filter import (
134
136
  from sarj_python_lint.rules.prefer_walrus_regex_match import PreferWalrusRegexMatch
135
137
  from sarj_python_lint.rules.prefer_walrus_stream_loop import PreferWalrusStreamLoop
136
138
  from sarj_python_lint.rules.production_derived_test_cases import ProductionDerivedTestCases
137
- from sarj_python_lint.rules.pydantic_at_boundaries import PydanticAtBoundaries
138
139
  from sarj_python_lint.rules.pytest_fixture_returns_bare_tuple import PytestFixtureReturnsBareTuple
139
140
  from sarj_python_lint.rules.redundant_class_docstring import RedundantClassDocstring
140
141
  from sarj_python_lint.rules.redundant_docstring import RedundantDocstring
@@ -189,7 +190,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
189
190
  NoExcessiveCognitiveComplexity.id: NoExcessiveCognitiveComplexity,
190
191
  PreferNamedtupleOverTupleReturn.id: PreferNamedtupleOverTupleReturn,
191
192
  NoCorsWildcardWithCredentials.id: NoCorsWildcardWithCredentials,
192
- PydanticAtBoundaries.id: PydanticAtBoundaries,
193
+ NamedRecordAtBoundaries.id: NamedRecordAtBoundaries,
193
194
  FastapiExplicitOpenapiContract.id: FastapiExplicitOpenapiContract,
194
195
  FastapiClassRouterContract.id: FastapiClassRouterContract,
195
196
  FakesInSharedLocation.id: FakesInSharedLocation,
@@ -243,6 +244,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
243
244
  PreferOrPattern.id: PreferOrPattern,
244
245
  RequirePortForService.id: RequirePortForService,
245
246
  PreferNonNullableCollection.id: PreferNonNullableCollection,
247
+ PreferMatchExceptionDispatch.id: PreferMatchExceptionDispatch,
246
248
  PreferMatchTypeDispatch.id: PreferMatchTypeDispatch,
247
249
  PreferMatchValueDispatch.id: PreferMatchValueDispatch,
248
250
  PreferWalrusRegexMatch.id: PreferWalrusRegexMatch,
@@ -267,7 +269,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
267
269
  NoUnnecessaryDocstring.id: NoUnnecessaryDocstring,
268
270
  NoUnusedValueMarker.id: NoUnusedValueMarker,
269
271
  PreferNominalIdTypes.id: PreferNominalIdTypes,
270
- NoVagueAnnotations.id: NoVagueAnnotations,
272
+ NoAnyMappingTypes.id: NoAnyMappingTypes,
271
273
  NoUniqueViolationMessageMatch.id: NoUniqueViolationMessageMatch,
272
274
  NegativeOnlyHttpStatusAssertion.id: NegativeOnlyHttpStatusAssertion,
273
275
  ProductionDerivedTestCases.id: ProductionDerivedTestCases,
@@ -425,18 +425,41 @@ def _schema_erasing(node: ast.expr, index: FastapiIndex) -> bool:
425
425
  return _schema_erasing(parts[0], index)
426
426
  if isinstance(resolved, ast.BinOp) and isinstance(resolved.op, ast.BitOr):
427
427
  return _schema_erasing(resolved.left, index) or _schema_erasing(resolved.right, index)
428
- name = flat_name(resolved.value) if isinstance(resolved, ast.Subscript) else flat_name(resolved)
429
- if name in {"Any", "object"}:
428
+ target = resolved.value if isinstance(resolved, ast.Subscript) else resolved
429
+ if _is_typing_type(target, index, "Any") or _is_builtin_type(target, index, "object"):
430
430
  return True
431
- if name in _RAW_MAPPINGS or name in _CONTAINERS:
431
+ if index.imports.resolves(target, sources=frozenset({"pydantic"}), symbol="JsonValue"):
432
+ return True
433
+ if _is_container_type(target, index, _RAW_MAPPINGS | _CONTAINERS):
432
434
  if not isinstance(resolved, ast.Subscript):
433
435
  return True
434
436
  return any(_schema_erasing(item, index) for item in _slice_items(resolved.slice))
435
- if isinstance(resolved, ast.Subscript) and name in {"Optional", "Union"}:
437
+ if isinstance(resolved, ast.Subscript) and any(
438
+ _is_typing_type(target, index, wrapper) for wrapper in ("Optional", "Union")
439
+ ):
436
440
  return any(_schema_erasing(item, index) for item in _slice_items(resolved.slice))
437
441
  return False
438
442
 
439
443
 
444
+ def _is_builtin_type(node: ast.expr, index: FastapiIndex, name: str) -> bool:
445
+ return (
446
+ isinstance(node, ast.Name) and node.id == name and index.imports.builtin_is_unshadowed(name)
447
+ ) or index.imports.resolves(node, sources=frozenset({"builtins"}), symbol=name)
448
+
449
+
450
+ def _is_typing_type(node: ast.expr, index: FastapiIndex, name: str) -> bool:
451
+ return index.imports.resolves(node, sources=frozenset({"typing", "typing_extensions"}), symbol=name)
452
+
453
+
454
+ def _is_container_type(node: ast.expr, index: FastapiIndex, names: frozenset[str]) -> bool:
455
+ builtin_names = {"dict", "list", "set", "tuple"}
456
+ return any(
457
+ (name in builtin_names and _is_builtin_type(node, index, name))
458
+ or index.imports.resolves(node, sources=frozenset({"collections.abc", "typing"}), symbol=name)
459
+ for name in names
460
+ )
461
+
462
+
440
463
  def _slice_items(node: ast.expr) -> tuple[ast.expr, ...]:
441
464
  return tuple(node.elts) if isinstance(node, ast.Tuple) else (node,)
442
465
 
@@ -36,15 +36,15 @@ _DICT_ARG_COUNT = 2
36
36
  _DOCUMENTATION_DIR_NAMES = frozenset({"docs", "docs_src"})
37
37
 
38
38
 
39
- class PydanticAtBoundaries(Rule):
40
- id: str = "pydantic-at-boundaries"
39
+ class NamedRecordAtBoundaries(Rule):
40
+ id: str = "named-record-at-boundaries"
41
41
  code: str = "SARJ008"
42
42
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
43
43
  summary="Public Python API returns an unnamed fixed-shape record.",
44
44
  rationale="A named record makes field types and required keys explicit to callers and static tooling.",
45
45
  remediation="Define and return a `TypedDict`, Pydantic model, or frozen dataclass for the fixed record shape.",
46
46
  category=RuleCategory.ARCHITECTURE,
47
- aliases=("named-fixed-record-return",),
47
+ aliases=("pydantic-at-boundaries", "named-fixed-record-return"),
48
48
  limitations=(
49
49
  "Visible FastAPI routes are owned by SARJ094. Private functions and classes, closures, tests, generated files, documentation examples, recognized framework hooks, and dictionary conversion methods are excluded.",
50
50
  "Only returned record literals and locally built fixed-shape dictionaries are recognized.",
@@ -57,7 +57,7 @@ class PydanticAtBoundaries(Rule):
57
57
  files=(
58
58
  ExampleFile.python(
59
59
  "service.py",
60
- "from typing import Any\n\ndef build_payload(call) -> dict[str, Any]:\n return {'id': call.id}\n",
60
+ "def build_payload(call) -> dict[str, object]:\n return {'id': call.id}\n",
61
61
  ),
62
62
  ),
63
63
  focus_path=PurePosixPath("service.py"),
@@ -250,24 +250,23 @@ def _classify_return(node: ast.expr, imports: ImportIndex) -> str | None:
250
250
  if _is_typing_type(node.value, imports, "Union"):
251
251
  return _classify_union_return(node, imports)
252
252
  if _is_type(node.value, imports, builtin="list", typing_symbol="List"):
253
- # Only list-of-untyped-dict is flagged (e.g. `list[dict[str, Any]]`).
253
+ # Fixed lists of unnamed records need one named element contract.
254
254
  inner = _classify_return(node.slice, imports)
255
255
  return "dict" if inner == "dict" else None
256
256
  if _is_type(node.value, imports, builtin="dict", typing_symbol="Dict"):
257
- return "dict" if _is_untyped_dict_args(node.slice, imports) else None
257
+ return "dict" if _is_named_record_dict_args(node.slice, imports) else None
258
258
  # Heterogeneous tuple returns are NOT flagged — multiple return values are
259
259
  # idiomatic Python, not a missing data contract.
260
260
  return None
261
261
 
262
262
 
263
- def _is_untyped_dict_args(slice_node: ast.expr, imports: ImportIndex) -> bool:
263
+ def _is_named_record_dict_args(slice_node: ast.expr, imports: ImportIndex) -> bool:
264
264
  if not isinstance(slice_node, ast.Tuple) or len(slice_node.elts) != _DICT_ARG_COUNT:
265
265
  return False
266
266
  key = _resolve_annotation(slice_node.elts[0])
267
267
  if key is None or not _is_builtin(key, imports, "str"):
268
268
  return False
269
- value = _resolve_annotation(slice_node.elts[1])
270
- return value is not None and (_is_builtin(value, imports, "object") or _is_typing_type(value, imports, "Any"))
269
+ return _resolve_annotation(slice_node.elts[1]) is not None
271
270
 
272
271
 
273
272
  def _is_type(node: ast.expr, imports: ImportIndex, *, builtin: str, typing_symbol: str) -> bool:
@@ -0,0 +1,257 @@
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
+ is_suppressed,
17
+ )
18
+ from sarj_python_lint.rules._imports import ImportIndex
19
+ from sarj_python_lint.rules._paths import is_generated
20
+
21
+
22
+ if TYPE_CHECKING:
23
+ from pathlib import Path
24
+
25
+
26
+ _TYPING_SOURCES = frozenset({"typing", "typing_extensions"})
27
+ _PYDANTIC_SOURCES = frozenset({"pydantic"})
28
+ _MAPPING_SOURCES = frozenset({"collections.abc", "typing"})
29
+ _MAPPING_NAMES = frozenset({"dict", "Dict", "Mapping", "MutableMapping"})
30
+ _MAPPING_ARGUMENT_COUNT = 2
31
+
32
+
33
+ @final
34
+ class NoAnyMappingTypes(Rule):
35
+ id = "no-any-mapping-types"
36
+ code = "SARJ447"
37
+ documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
38
+ summary="String-keyed mapping types must not erase their values with `Any`.",
39
+ rationale=(
40
+ "An `Any`-valued mapping disables checking transitively and conceals whether the value is a fixed record, "
41
+ "an arbitrary JSON tree, or a genuinely opaque Python object."
42
+ ),
43
+ remediation=(
44
+ "Use `TypedDict` (and `Unpack[TypedDict]` for closed keyword arguments) for fixed records, a Pydantic model "
45
+ "for untrusted runtime input, `pydantic.JsonValue` for arbitrary JSON, or `object` plus explicit narrowing "
46
+ "for a genuinely opaque Python value."
47
+ ),
48
+ category=RuleCategory.CORRECTNESS,
49
+ autofix=AutofixPolicy.NONE,
50
+ aliases=("no-vague-annotations",),
51
+ limitations=(
52
+ (
53
+ "Only proven built-in or standard-library string-keyed mappings whose value recursively contains "
54
+ "`typing.Any` are reported; `object` is intentionally not treated as `Any`."
55
+ ),
56
+ "Generated and vendored sources are excluded; exact local suppressions remain auditable.",
57
+ ),
58
+ examples=(
59
+ RuleExample(
60
+ example_id="any-valued-mapping-annotation",
61
+ title="An Any-valued mapping erases the record schema",
62
+ outcome=ExampleOutcome.MATCH,
63
+ files=(
64
+ ExampleFile.python(
65
+ "app/offers.py", "from typing import Any\ndef details() -> dict[str, Any]: ...\n"
66
+ ),
67
+ ),
68
+ focus_path=PurePosixPath("app/offers.py"),
69
+ expected_count=1,
70
+ public=True,
71
+ ),
72
+ RuleExample(
73
+ example_id="typed-dictionary-annotation",
74
+ title="A TypedDict preserves the record schema",
75
+ outcome=ExampleOutcome.NO_MATCH,
76
+ files=(
77
+ ExampleFile.python(
78
+ "app/offers.py",
79
+ "from typing import TypedDict\nclass OfferDetails(TypedDict):\n id: str\ndef details() -> OfferDetails: ...\n",
80
+ ),
81
+ ),
82
+ focus_path=PurePosixPath("app/offers.py"),
83
+ expected_count=0,
84
+ public=True,
85
+ ),
86
+ ),
87
+ )
88
+ description = documentation.summary
89
+
90
+ @override
91
+ def check(self, path: Path, source: str) -> list[Diagnostic]:
92
+ if is_generated(path, source) or _is_vendor_path(path):
93
+ return []
94
+ try:
95
+ tree = ast.parse(source, filename=str(path), type_comments=True)
96
+ except SyntaxError:
97
+ return []
98
+ imports = ImportIndex.from_tree(tree, module_scope_only=True)
99
+ aliases = _type_aliases(tree, imports)
100
+ lines = source.splitlines()
101
+ findings: list[Diagnostic] = []
102
+ seen: set[tuple[int, int]] = set()
103
+ for expression in _type_expressions(tree, imports):
104
+ for mapping in _any_mappings(expression, imports, aliases):
105
+ location = expression if isinstance(expression, ast.Constant) else mapping
106
+ key = (location.lineno, location.col_offset)
107
+ if key in seen or is_suppressed(lines, location.lineno, self.code):
108
+ continue
109
+ seen.add(key)
110
+ findings.append(
111
+ Diagnostic(
112
+ path=path,
113
+ line=location.lineno,
114
+ col=location.col_offset + 1,
115
+ code=self.code,
116
+ message=(
117
+ f"mapping type `{ast.unparse(mapping)}` contains `Any`; use `TypedDict` for a fixed "
118
+ "record, `pydantic.JsonValue` for arbitrary JSON, or `object` with narrowing for opaque values"
119
+ ),
120
+ )
121
+ )
122
+ return sorted(findings, key=lambda finding: (finding.line, finding.col))
123
+
124
+
125
+ def _type_expressions(tree: ast.Module, imports: ImportIndex) -> list[ast.expr]:
126
+ expressions = [annotation for node in ast.walk(tree) if (annotation := _node_annotation(node, imports)) is not None]
127
+ for node in ast.walk(tree):
128
+ if not isinstance(node, ast.Call) or not node.args:
129
+ continue
130
+ if imports.resolves(node.func, sources=_TYPING_SOURCES, symbol="cast") or imports.resolves(
131
+ node.func, sources=_PYDANTIC_SOURCES, symbol="TypeAdapter"
132
+ ):
133
+ expressions.append(node.args[0])
134
+ return expressions
135
+
136
+
137
+ def _node_annotation(node: ast.AST, imports: ImportIndex) -> ast.expr | None:
138
+ match node:
139
+ case (
140
+ ast.arg(annotation=annotation)
141
+ | ast.FunctionDef(returns=annotation)
142
+ | ast.AsyncFunctionDef(returns=annotation)
143
+ ):
144
+ return annotation
145
+ case ast.AnnAssign(annotation=annotation, value=value) if value is not None:
146
+ if imports.resolves(annotation, sources=_TYPING_SOURCES, symbol="TypeAlias"):
147
+ return value
148
+ return annotation
149
+ case ast.AnnAssign(annotation=annotation):
150
+ return annotation
151
+ case ast.TypeAlias(value=value):
152
+ return value
153
+ case (
154
+ ast.Assign(type_comment=str(comment))
155
+ | ast.For(type_comment=str(comment))
156
+ | ast.AsyncFor(type_comment=str(comment))
157
+ | ast.With(type_comment=str(comment))
158
+ | ast.AsyncWith(type_comment=str(comment))
159
+ ):
160
+ return _type_comment(comment, node)
161
+ case _:
162
+ return None
163
+
164
+
165
+ def _type_comment(comment: str, owner: ast.AST) -> ast.expr | None:
166
+ try:
167
+ expression = ast.parse(comment, mode="eval").body
168
+ except SyntaxError:
169
+ return None
170
+ ast.copy_location(expression, owner)
171
+ return expression
172
+
173
+
174
+ def _parse_string_annotation(annotation: ast.expr) -> ast.expr:
175
+ if not isinstance(annotation, ast.Constant) or not isinstance(annotation.value, str):
176
+ return annotation
177
+ try:
178
+ parsed = ast.parse(annotation.value, mode="eval").body
179
+ except SyntaxError:
180
+ return annotation
181
+ ast.copy_location(parsed, annotation)
182
+ return parsed
183
+
184
+
185
+ def _any_mappings(annotation: ast.expr, imports: ImportIndex, aliases: dict[str, ast.expr]) -> list[ast.Subscript]:
186
+ resolved = _parse_string_annotation(annotation)
187
+ return [
188
+ node
189
+ for node in ast.walk(resolved)
190
+ if isinstance(node, ast.Subscript) and _is_any_mapping(node, imports, aliases)
191
+ ]
192
+
193
+
194
+ def _is_any_mapping(node: ast.Subscript, imports: ImportIndex, aliases: dict[str, ast.expr]) -> bool:
195
+ if not isinstance(node.slice, ast.Tuple) or len(node.slice.elts) != _MAPPING_ARGUMENT_COUNT:
196
+ return False
197
+ target = node.value
198
+ mapping = (isinstance(target, ast.Name) and target.id == "dict" and imports.builtin_is_unshadowed("dict")) or any(
199
+ imports.resolves(target, sources=_MAPPING_SOURCES, symbol=name) for name in _MAPPING_NAMES
200
+ )
201
+ if not mapping:
202
+ return False
203
+ key, value = node.slice.elts
204
+ return _is_builtin_str(key, imports) and _contains_any(value, imports, aliases, seen=frozenset())
205
+
206
+
207
+ def _is_builtin_str(node: ast.expr, imports: ImportIndex) -> bool:
208
+ return isinstance(node, ast.Name) and node.id == "str" and imports.builtin_is_unshadowed("str")
209
+
210
+
211
+ def _contains_any(
212
+ node: ast.expr,
213
+ imports: ImportIndex,
214
+ aliases: dict[str, ast.expr],
215
+ *,
216
+ seen: frozenset[str],
217
+ ) -> bool:
218
+ node = _parse_string_annotation(node)
219
+ if imports.resolves(node, sources=_TYPING_SOURCES, symbol="Any"):
220
+ return True
221
+ if isinstance(node, ast.Name) and node.id in aliases and node.id not in seen:
222
+ return _contains_any(aliases[node.id], imports, aliases, seen=seen | {node.id})
223
+ return any(
224
+ _contains_any(child, imports, aliases, seen=seen)
225
+ for child in ast.iter_child_nodes(node)
226
+ if isinstance(child, ast.expr)
227
+ )
228
+
229
+
230
+ def _type_aliases(tree: ast.Module, imports: ImportIndex) -> dict[str, ast.expr]:
231
+ aliases: dict[str, ast.expr] = {}
232
+ ambiguous: set[str] = set()
233
+ for statement in tree.body:
234
+ name: str | None = None
235
+ value: ast.expr | None = None
236
+ match statement:
237
+ case ast.TypeAlias(name=ast.Name(id=alias), value=alias_value, type_params=[]):
238
+ name, value = alias, alias_value
239
+ case ast.AnnAssign(target=ast.Name(id=alias), annotation=annotation, value=alias_value) if (
240
+ alias_value is not None and imports.resolves(annotation, sources=_TYPING_SOURCES, symbol="TypeAlias")
241
+ ):
242
+ name, value = alias, alias_value
243
+ case _:
244
+ pass
245
+ if name is None or value is None:
246
+ continue
247
+ if name in aliases:
248
+ ambiguous.add(name)
249
+ else:
250
+ aliases[name] = value
251
+ for name in ambiguous:
252
+ aliases.pop(name, None)
253
+ return aliases
254
+
255
+
256
+ def _is_vendor_path(path: Path) -> bool:
257
+ return any(part.casefold() in {"vendor", "vendored", "third_party"} for part in path.parts)