sarj-python-lint 0.85.0__tar.gz → 0.86.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.85.0 → sarj_python_lint-0.86.0}/PKG-INFO +1 -1
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/pyproject.toml +1 -1
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/__main__.py +45 -13
- sarj_python_lint-0.86.0/src/sarj_python_lint/rules/_annotation_semantics.py +174 -0
- sarj_python_lint-0.86.0/src/sarj_python_lint/rules/_fixed_record.py +578 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_registry.py +2 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +5 -1
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +81 -20
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +108 -63
- sarj_python_lint-0.86.0/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +306 -0
- sarj_python_lint-0.85.0/src/sarj_python_lint/rules/_fixed_record.py +0 -170
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/.gitignore +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/LICENSE +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/README.md +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/require_typed_http_test_response.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-0.85.0 → sarj_python_lint-0.86.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.86.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/
|
|
@@ -142,6 +142,10 @@ _DIAGNOSTIC_PRECEDENCE = MappingProxyType(
|
|
|
142
142
|
"SARJ088": frozenset({"SARJ050", "SARJ085", "SARJ420"}),
|
|
143
143
|
"SARJ092": frozenset({"SARJ086", "SARJ087", "SARJ420"}),
|
|
144
144
|
"SARJ093": frozenset({"SARJ034"}),
|
|
145
|
+
# Visible FastAPI operations have a route-aware contract diagnostic.
|
|
146
|
+
# Keep the annotation diagnostics when SARJ094 is absent, including
|
|
147
|
+
# when a concrete response_model makes the route contract complete.
|
|
148
|
+
"SARJ094": frozenset({"SARJ008", "SARJ447"}),
|
|
145
149
|
"SARJ099": frozenset({"SARJ420"}),
|
|
146
150
|
# Any-valued mappings are the stronger correctness failure. SARJ008
|
|
147
151
|
# still owns every fixed dictionary return when selected alone, while
|
|
@@ -166,13 +170,17 @@ def deduplicate_diagnostics(diags: list[Diagnostic], *, source: str | None = Non
|
|
|
166
170
|
"SARJ420" in codes and not codes.isdisjoint(_DOCSTRING_PRECEDENCE_CODES)
|
|
167
171
|
)
|
|
168
172
|
docstring_owners = _docstring_owner_locations(source) if source is not None and needs_docstring_owners else {}
|
|
169
|
-
needs_signature_owners = (
|
|
173
|
+
needs_signature_owners = (
|
|
174
|
+
("SARJ093" in codes and "SARJ034" in codes)
|
|
175
|
+
or ("SARJ447" in codes and "SARJ008" in codes)
|
|
176
|
+
or ("SARJ094" in codes and not codes.isdisjoint(_DIAGNOSTIC_PRECEDENCE["SARJ094"]))
|
|
177
|
+
)
|
|
170
178
|
signature_owners = (
|
|
171
179
|
_function_signature_owner_locations(source) if source is not None and needs_signature_owners else {}
|
|
172
180
|
)
|
|
173
181
|
|
|
174
182
|
def owner_location(diagnostic: Diagnostic) -> _OwnerLocation:
|
|
175
|
-
if diagnostic.code in {"SARJ008", "SARJ034", "SARJ093", "SARJ447"}:
|
|
183
|
+
if diagnostic.code in {"SARJ008", "SARJ034", "SARJ093", "SARJ094", "SARJ447"}:
|
|
176
184
|
line, column = signature_owners.get(diagnostic.line, (diagnostic.line, diagnostic.col))
|
|
177
185
|
else:
|
|
178
186
|
line, column = docstring_owners.get(diagnostic.line, (diagnostic.line, diagnostic.col))
|
|
@@ -180,6 +188,8 @@ def deduplicate_diagnostics(diags: list[Diagnostic], *, source: str | None = Non
|
|
|
180
188
|
|
|
181
189
|
present: dict[tuple[Path, int, int], dict[str, set[Severity]]] = {}
|
|
182
190
|
for diagnostic in diags:
|
|
191
|
+
if diagnostic.code == "SARJ094" and not diagnostic.message.startswith("[return]"):
|
|
192
|
+
continue
|
|
183
193
|
location = owner_location(diagnostic)
|
|
184
194
|
by_code = present.setdefault((diagnostic.path, location.line, location.column), {})
|
|
185
195
|
by_code.setdefault(diagnostic.code, set()).add(diagnostic.severity)
|
|
@@ -213,23 +223,45 @@ def _suppressed_diagnostics(
|
|
|
213
223
|
|
|
214
224
|
|
|
215
225
|
def _suppressed_codes(codes: dict[str, set[Severity]]) -> set[tuple[str, Severity]]:
|
|
216
|
-
suppressed =
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
226
|
+
suppressed = _precedence_suppressions(codes)
|
|
227
|
+
# SARJ094 is deliberately staged as a warning while teams calibrate the
|
|
228
|
+
# FastAPI contract policy. Its route-aware return finding still owns the
|
|
229
|
+
# overlapping annotation failure: emitting the blocking SARJ447 twin would
|
|
230
|
+
# defeat warning staging and give two remediations for one return contract.
|
|
231
|
+
if "SARJ094" in codes:
|
|
232
|
+
suppressed.update(_route_contract_suppressions(codes))
|
|
224
233
|
# The promoted signature-restatement rules now block. Keep their errors and
|
|
225
234
|
# drop only the lower-severity typed-docstring twin at the same owner.
|
|
226
|
-
if
|
|
227
|
-
Severity.ERROR in codes.get(generic, set()) for generic in ("SARJ086", "SARJ087")
|
|
228
|
-
):
|
|
235
|
+
if _typed_docstring_warning_is_superseded(codes):
|
|
229
236
|
suppressed.add(("SARJ092", Severity.WARNING))
|
|
230
237
|
return suppressed
|
|
231
238
|
|
|
232
239
|
|
|
240
|
+
def _precedence_suppressions(codes: dict[str, set[Severity]]) -> set[tuple[str, Severity]]:
|
|
241
|
+
suppressed: set[tuple[str, Severity]] = set()
|
|
242
|
+
for specific, generics in _DIAGNOSTIC_PRECEDENCE.items():
|
|
243
|
+
if specific not in codes:
|
|
244
|
+
continue
|
|
245
|
+
specific_severities = codes[specific]
|
|
246
|
+
for generic in generics:
|
|
247
|
+
for generic_severity in codes.get(generic, set()):
|
|
248
|
+
if generic_severity is Severity.WARNING or Severity.ERROR in specific_severities:
|
|
249
|
+
suppressed.add((generic, generic_severity))
|
|
250
|
+
return suppressed
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _route_contract_suppressions(codes: dict[str, set[Severity]]) -> set[tuple[str, Severity]]:
|
|
254
|
+
return {
|
|
255
|
+
(generic, severity) for generic in _DIAGNOSTIC_PRECEDENCE["SARJ094"] for severity in codes.get(generic, set())
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _typed_docstring_warning_is_superseded(codes: dict[str, set[Severity]]) -> bool:
|
|
260
|
+
if Severity.WARNING not in codes.get("SARJ092", set()):
|
|
261
|
+
return False
|
|
262
|
+
return any(Severity.ERROR in codes.get(generic, set()) for generic in ("SARJ086", "SARJ087"))
|
|
263
|
+
|
|
264
|
+
|
|
233
265
|
def _function_signature_owner_locations(source: str) -> dict[int, _OwnerLocation]:
|
|
234
266
|
try:
|
|
235
267
|
tree = ast.parse(source)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from types import MappingProxyType
|
|
6
|
+
from typing import Self
|
|
7
|
+
|
|
8
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_TYPING_SOURCES = frozenset({"typing", "typing_extensions"})
|
|
12
|
+
_TRANSPARENT_WRAPPERS = frozenset({"Annotated", "ClassVar", "Final", "NotRequired", "Optional", "ReadOnly", "Required"})
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class AnnotationSemantics:
|
|
17
|
+
imports: ImportIndex
|
|
18
|
+
aliases: MappingProxyType[str, ast.expr]
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_tree(cls, tree: ast.Module, *, module_scope_only: bool = True) -> Self:
|
|
22
|
+
imports = ImportIndex.from_tree(tree, module_scope_only=module_scope_only)
|
|
23
|
+
candidates: dict[str, ast.expr] = {}
|
|
24
|
+
ambiguous: set[str] = set()
|
|
25
|
+
for statement in tree.body:
|
|
26
|
+
name: str | None = None
|
|
27
|
+
value: ast.expr | None = None
|
|
28
|
+
match statement:
|
|
29
|
+
case ast.TypeAlias(name=ast.Name(id=alias), value=alias_value, type_params=[]):
|
|
30
|
+
name, value = alias, alias_value
|
|
31
|
+
case ast.AnnAssign(target=ast.Name(id=alias), annotation=annotation, value=alias_value) if (
|
|
32
|
+
alias_value is not None
|
|
33
|
+
and imports.resolves(annotation, sources=_TYPING_SOURCES, symbol="TypeAlias")
|
|
34
|
+
):
|
|
35
|
+
name, value = alias, alias_value
|
|
36
|
+
case _:
|
|
37
|
+
pass
|
|
38
|
+
if name is None or value is None:
|
|
39
|
+
continue
|
|
40
|
+
if name in candidates:
|
|
41
|
+
ambiguous.add(name)
|
|
42
|
+
else:
|
|
43
|
+
candidates[name] = value
|
|
44
|
+
if not candidates:
|
|
45
|
+
return cls(imports, MappingProxyType({}))
|
|
46
|
+
rebound = _module_rebindings(tree)
|
|
47
|
+
aliases = {name: value for name, value in candidates.items() if name not in ambiguous and name not in rebound}
|
|
48
|
+
return cls(imports, MappingProxyType(aliases))
|
|
49
|
+
|
|
50
|
+
def parse(self, annotation: ast.expr | None) -> ast.expr | None:
|
|
51
|
+
if not isinstance(annotation, ast.Constant) or not isinstance(annotation.value, str):
|
|
52
|
+
return annotation
|
|
53
|
+
try:
|
|
54
|
+
parsed = ast.parse(annotation.value.strip(), mode="eval").body
|
|
55
|
+
except SyntaxError:
|
|
56
|
+
return None
|
|
57
|
+
ast.copy_location(parsed, annotation)
|
|
58
|
+
return parsed
|
|
59
|
+
|
|
60
|
+
def resolve_alias(self, annotation: ast.expr | None, *, seen: frozenset[str] = frozenset()) -> ast.expr | None:
|
|
61
|
+
resolved = self.parse(annotation)
|
|
62
|
+
while isinstance(resolved, ast.Name) and resolved.id in self.aliases and resolved.id not in seen:
|
|
63
|
+
seen |= {resolved.id}
|
|
64
|
+
resolved = self.parse(self.aliases[resolved.id])
|
|
65
|
+
return resolved
|
|
66
|
+
|
|
67
|
+
def transparent_members(
|
|
68
|
+
self, annotation: ast.expr | None, *, seen: frozenset[str] = frozenset()
|
|
69
|
+
) -> tuple[ast.expr, ...]:
|
|
70
|
+
resolved = self.parse(annotation)
|
|
71
|
+
if resolved is None:
|
|
72
|
+
return ()
|
|
73
|
+
if isinstance(resolved, ast.Name) and resolved.id in self.aliases:
|
|
74
|
+
return self._alias_members(resolved.id, seen=seen)
|
|
75
|
+
if isinstance(resolved, ast.BinOp) and isinstance(resolved.op, ast.BitOr):
|
|
76
|
+
return self.transparent_members(resolved.left, seen=seen) + self.transparent_members(
|
|
77
|
+
resolved.right, seen=seen
|
|
78
|
+
)
|
|
79
|
+
if isinstance(resolved, ast.Subscript):
|
|
80
|
+
return self._subscript_members(resolved, seen=seen)
|
|
81
|
+
return (resolved,)
|
|
82
|
+
|
|
83
|
+
def _alias_members(self, name: str, *, seen: frozenset[str]) -> tuple[ast.expr, ...]:
|
|
84
|
+
if name in seen:
|
|
85
|
+
return ()
|
|
86
|
+
return self.transparent_members(self.aliases[name], seen=seen | {name})
|
|
87
|
+
|
|
88
|
+
def _subscript_members(self, node: ast.Subscript, *, seen: frozenset[str]) -> tuple[ast.expr, ...]:
|
|
89
|
+
wrapper = self.imports.resolved_symbol(node.value, sources=_TYPING_SOURCES)
|
|
90
|
+
if wrapper == "Union":
|
|
91
|
+
members = node.slice.elts if isinstance(node.slice, ast.Tuple) else [node.slice]
|
|
92
|
+
return self._flatten_members(members, seen=seen)
|
|
93
|
+
if wrapper not in _TRANSPARENT_WRAPPERS:
|
|
94
|
+
return (node,)
|
|
95
|
+
inner = node.slice.elts[0] if isinstance(node.slice, ast.Tuple) else node.slice
|
|
96
|
+
return self.transparent_members(inner, seen=seen)
|
|
97
|
+
|
|
98
|
+
def _flatten_members(self, nodes: list[ast.expr], *, seen: frozenset[str]) -> tuple[ast.expr, ...]:
|
|
99
|
+
members: list[ast.expr] = []
|
|
100
|
+
for node in nodes:
|
|
101
|
+
members.extend(self.transparent_members(node, seen=seen))
|
|
102
|
+
return tuple(members)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _module_rebindings(tree: ast.Module) -> set[str]:
|
|
106
|
+
counts: dict[str, int] = {}
|
|
107
|
+
for statement in tree.body:
|
|
108
|
+
for name in _bound_names(statement):
|
|
109
|
+
counts[name] = counts.get(name, 0) + 1
|
|
110
|
+
return {name for name, count in counts.items() if count > 1}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def scope_bound_names(statements: list[ast.stmt]) -> set[str]:
|
|
114
|
+
return {name for statement in statements for name in _bound_names(statement)}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _bound_names(statement: ast.stmt) -> set[str]:
|
|
118
|
+
collector = _ScopeBindings()
|
|
119
|
+
collector.visit(statement)
|
|
120
|
+
return collector.names
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _target_names(node: ast.expr) -> set[str]:
|
|
124
|
+
if isinstance(node, ast.Name):
|
|
125
|
+
return {node.id}
|
|
126
|
+
if isinstance(node, (ast.List, ast.Tuple)):
|
|
127
|
+
return {name for item in node.elts for name in _target_names(item)}
|
|
128
|
+
return set()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class _ScopeBindings(ast.NodeVisitor):
|
|
132
|
+
def __init__(self) -> None:
|
|
133
|
+
self.names: set[str] = set()
|
|
134
|
+
|
|
135
|
+
def visit_Name(self, node: ast.Name) -> None:
|
|
136
|
+
if isinstance(node.ctx, (ast.Store, ast.Del)):
|
|
137
|
+
self.names.add(node.id)
|
|
138
|
+
|
|
139
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
140
|
+
self.names.add(node.name)
|
|
141
|
+
|
|
142
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
143
|
+
self.names.add(node.name)
|
|
144
|
+
|
|
145
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
|
146
|
+
self.names.add(node.name)
|
|
147
|
+
|
|
148
|
+
def visit_Lambda(self, node: ast.Lambda) -> None:
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
def visit_Import(self, node: ast.Import) -> None:
|
|
152
|
+
self.names.update(alias.asname or alias.name.partition(".")[0] for alias in node.names)
|
|
153
|
+
|
|
154
|
+
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
|
|
155
|
+
self.names.update(alias.asname or alias.name for alias in node.names if alias.name != "*")
|
|
156
|
+
|
|
157
|
+
def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None:
|
|
158
|
+
if node.name is not None:
|
|
159
|
+
self.names.add(node.name)
|
|
160
|
+
self.generic_visit(node)
|
|
161
|
+
|
|
162
|
+
def visit_MatchAs(self, node: ast.MatchAs) -> None:
|
|
163
|
+
if node.name is not None:
|
|
164
|
+
self.names.add(node.name)
|
|
165
|
+
self.generic_visit(node)
|
|
166
|
+
|
|
167
|
+
def visit_MatchStar(self, node: ast.MatchStar) -> None:
|
|
168
|
+
if node.name is not None:
|
|
169
|
+
self.names.add(node.name)
|
|
170
|
+
|
|
171
|
+
def visit_MatchMapping(self, node: ast.MatchMapping) -> None:
|
|
172
|
+
if node.rest is not None:
|
|
173
|
+
self.names.add(node.rest)
|
|
174
|
+
self.generic_visit(node)
|