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.
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/PKG-INFO +1 -1
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/pyproject.toml +1 -1
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/__main__.py +6 -2
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_fastapi.py +3 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_imports.py +6 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_registry.py +6 -4
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +27 -4
- 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
- sarj_python_lint-0.85.0/src/sarj_python_lint/rules/no_any_mapping_types.py +257 -0
- sarj_python_lint-0.85.0/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +420 -0
- {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
- {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
- sarj_python_lint-0.84.3/src/sarj_python_lint/rules/no_vague_annotations.py +0 -173
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/.gitignore +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/LICENSE +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/README.md +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {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
- {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
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {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
- {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
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {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
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {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
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {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
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {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
- {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
- {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
- {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
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {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
- {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
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {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
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {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
- {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
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {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
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {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
- {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
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {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
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {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
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {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
- {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
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {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
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {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
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {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
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {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
- {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
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {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
- {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
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {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
- {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
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.84.3 → sarj_python_lint-0.85.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {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.
|
|
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/
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
429
|
-
if
|
|
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
|
|
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
|
|
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
|
|
40
|
-
id: str = "
|
|
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
|
-
"
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|