sarj-python-lint 1.2.1__tar.gz → 1.2.6__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-1.2.1 → sarj_python_lint-1.2.6}/PKG-INFO +1 -1
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/pyproject.toml +1 -1
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/_analysis_session.py +2 -0
- sarj_python_lint-1.2.6/src/sarj_python_lint/rules/_nominal_project.py +280 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_registry.py +2 -0
- sarj_python_lint-1.2.6/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +227 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_unused_value_marker.py +25 -55
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_match_assert_never.py +53 -3
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +302 -24
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +248 -19
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/.gitignore +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/LICENSE +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/README.md +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.2.1 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.6
|
|
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/
|
|
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, final
|
|
|
4
4
|
|
|
5
5
|
from sarj_python_lint._python_target import PythonTargetFacts
|
|
6
6
|
from sarj_python_lint.rules._first_party import FirstPartyFacts
|
|
7
|
+
from sarj_python_lint.rules._nominal_project import NominalProjectFacts
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
if TYPE_CHECKING:
|
|
@@ -16,3 +17,4 @@ class AnalysisSession:
|
|
|
16
17
|
self.project: ProjectIndexSet | None = None
|
|
17
18
|
self.first_party = FirstPartyFacts()
|
|
18
19
|
self.python_target = PythonTargetFacts()
|
|
20
|
+
self.nominal_project = NominalProjectFacts()
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import tomllib
|
|
8
|
+
from types import MappingProxyType
|
|
9
|
+
from typing import TYPE_CHECKING, TypeGuard, final
|
|
10
|
+
|
|
11
|
+
from packaging.requirements import InvalidRequirement, Requirement
|
|
12
|
+
from packaging.utils import canonicalize_name
|
|
13
|
+
|
|
14
|
+
from sarj_python_lint.rules._first_party import distribution_root
|
|
15
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
16
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from sarj_python_lint.rules._first_party import FirstPartyFacts
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_MAX_FILES = 5_000
|
|
24
|
+
_MAX_DIRECTORIES = 3_000
|
|
25
|
+
_MAX_BYTES = 500_000
|
|
26
|
+
_MAX_TOTAL_BYTES = 25_000_000
|
|
27
|
+
_SKIP = frozenset({"node_modules", "vendor", "vendored", "build", "dist", "__pycache__", "venv"})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class NominalSource:
|
|
32
|
+
path: Path
|
|
33
|
+
text: str
|
|
34
|
+
|
|
35
|
+
@cached_property
|
|
36
|
+
def tree(self) -> ast.Module | None:
|
|
37
|
+
try:
|
|
38
|
+
tree = ast.parse(self.text, filename=str(self.path))
|
|
39
|
+
except SyntaxError:
|
|
40
|
+
return None
|
|
41
|
+
if any(
|
|
42
|
+
isinstance(node, ast.ImportFrom) and any(alias.name == "*" for alias in node.names)
|
|
43
|
+
for node in ast.walk(tree)
|
|
44
|
+
):
|
|
45
|
+
return None
|
|
46
|
+
return tree
|
|
47
|
+
|
|
48
|
+
@cached_property
|
|
49
|
+
def imports(self) -> ImportIndex:
|
|
50
|
+
tree = self.tree
|
|
51
|
+
if tree is None:
|
|
52
|
+
return ImportIndex(MappingProxyType({}), frozenset())
|
|
53
|
+
imports = ImportIndex.from_tree(tree)
|
|
54
|
+
mutated = {
|
|
55
|
+
ast.unparse(node).partition(".")[0]
|
|
56
|
+
for node in ast.walk(tree)
|
|
57
|
+
if isinstance(node, ast.Attribute) and isinstance(node.ctx, (ast.Store, ast.Del))
|
|
58
|
+
}
|
|
59
|
+
return ImportIndex(
|
|
60
|
+
MappingProxyType({name: target for name, target in imports.bindings.items() if name not in mutated}),
|
|
61
|
+
imports.shadowed_names | mutated,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True, slots=True)
|
|
66
|
+
class _Workspace:
|
|
67
|
+
root: Path
|
|
68
|
+
members: tuple[str, ...]
|
|
69
|
+
excludes: tuple[str, ...]
|
|
70
|
+
|
|
71
|
+
def contains(self, path: Path) -> bool:
|
|
72
|
+
if path == self.root:
|
|
73
|
+
return True
|
|
74
|
+
if not path.is_relative_to(self.root):
|
|
75
|
+
return False
|
|
76
|
+
relative = path.relative_to(self.root)
|
|
77
|
+
return any(relative.full_match(pattern) for pattern in self.members) and not any(
|
|
78
|
+
relative.full_match(pattern) for pattern in self.excludes
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True, slots=True)
|
|
83
|
+
class _Distribution:
|
|
84
|
+
root: Path
|
|
85
|
+
name: str | None
|
|
86
|
+
dependencies: frozenset[str]
|
|
87
|
+
workspace: _Workspace | None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@final
|
|
91
|
+
class NominalProjectFacts:
|
|
92
|
+
# One request owns these caches; subsequent requests see source changes.
|
|
93
|
+
def __init__(self) -> None:
|
|
94
|
+
self._distributions: dict[Path, tuple[_Distribution, ...]] = {}
|
|
95
|
+
self._sources: dict[Path, dict[str, NominalSource]] = {}
|
|
96
|
+
self._scopes: dict[Path, dict[str, NominalSource]] = {}
|
|
97
|
+
|
|
98
|
+
def sources(self, path: Path, first_party: FirstPartyFacts) -> dict[str, NominalSource]:
|
|
99
|
+
owner = distribution_root(path, facts=first_party)
|
|
100
|
+
root = first_party.project_root(path)
|
|
101
|
+
if owner is None or root is None:
|
|
102
|
+
return {}
|
|
103
|
+
if owner in self._scopes:
|
|
104
|
+
return self._scopes[owner]
|
|
105
|
+
distributions = self._accessible_distributions(owner, root)
|
|
106
|
+
sources = self._merge_sources(distributions)
|
|
107
|
+
self._scopes[owner] = sources
|
|
108
|
+
return sources
|
|
109
|
+
|
|
110
|
+
def _accessible_distributions(self, owner: Path, root: Path) -> tuple[_Distribution, ...]:
|
|
111
|
+
if root not in self._distributions:
|
|
112
|
+
self._distributions[root] = tuple(
|
|
113
|
+
distribution
|
|
114
|
+
for manifest in _files(root, "pyproject.toml")
|
|
115
|
+
if (distribution := _distribution(manifest)) is not None
|
|
116
|
+
)
|
|
117
|
+
distributions = self._distributions[root]
|
|
118
|
+
own = next(
|
|
119
|
+
(
|
|
120
|
+
distribution
|
|
121
|
+
for distribution in distributions
|
|
122
|
+
if distribution.root == owner and distribution.name is not None
|
|
123
|
+
),
|
|
124
|
+
None,
|
|
125
|
+
)
|
|
126
|
+
if own is None:
|
|
127
|
+
return ()
|
|
128
|
+
distributions = _workspace_distributions(distributions, owner)
|
|
129
|
+
return tuple(
|
|
130
|
+
distribution
|
|
131
|
+
for distribution in distributions
|
|
132
|
+
if distribution == own
|
|
133
|
+
or (
|
|
134
|
+
distribution.name in own.dependencies
|
|
135
|
+
and sum(candidate.name == distribution.name for candidate in distributions) == 1
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def _merge_sources(self, distributions: tuple[_Distribution, ...]) -> dict[str, NominalSource]:
|
|
140
|
+
sources: dict[str, NominalSource] = {}
|
|
141
|
+
ambiguous: set[str] = set()
|
|
142
|
+
for distribution in distributions:
|
|
143
|
+
if distribution.root not in self._sources:
|
|
144
|
+
self._sources[distribution.root] = _sources(distribution.root)
|
|
145
|
+
for module, source in self._sources[distribution.root].items():
|
|
146
|
+
if module in sources:
|
|
147
|
+
ambiguous.add(module)
|
|
148
|
+
sources[module] = source
|
|
149
|
+
return {module: source for module, source in sources.items() if module not in ambiguous}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _workspace_distributions(distributions: tuple[_Distribution, ...], owner: Path) -> tuple[_Distribution, ...]:
|
|
153
|
+
workspaces = [
|
|
154
|
+
distribution.workspace
|
|
155
|
+
for distribution in distributions
|
|
156
|
+
if distribution.workspace is not None and distribution.workspace.contains(owner)
|
|
157
|
+
]
|
|
158
|
+
if workspaces:
|
|
159
|
+
workspace = max(workspaces, key=lambda scope: len(scope.root.parts))
|
|
160
|
+
distributions = tuple(distribution for distribution in distributions if workspace.contains(distribution.root))
|
|
161
|
+
return distributions
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _distribution(manifest: Path) -> _Distribution | None:
|
|
165
|
+
try:
|
|
166
|
+
if manifest.is_symlink() or manifest.stat().st_size > _MAX_BYTES:
|
|
167
|
+
return None
|
|
168
|
+
document: object = tomllib.loads(manifest.read_text(encoding="utf-8"))
|
|
169
|
+
except OSError, ValueError:
|
|
170
|
+
return None
|
|
171
|
+
if not _is_mapping(document):
|
|
172
|
+
return None
|
|
173
|
+
workspace = _workspace(manifest.parent, document)
|
|
174
|
+
project = document.get("project")
|
|
175
|
+
if not _is_mapping(project):
|
|
176
|
+
return _Distribution(manifest.parent, None, frozenset(), workspace) if workspace is not None else None
|
|
177
|
+
name = project.get("name")
|
|
178
|
+
dependencies = project.get("dependencies", [])
|
|
179
|
+
if not isinstance(name, str) or not _is_list(dependencies):
|
|
180
|
+
return None
|
|
181
|
+
names: set[str] = set()
|
|
182
|
+
for dependency in dependencies:
|
|
183
|
+
if not isinstance(dependency, str):
|
|
184
|
+
continue
|
|
185
|
+
try:
|
|
186
|
+
names.add(canonicalize_name(Requirement(dependency).name))
|
|
187
|
+
except InvalidRequirement:
|
|
188
|
+
continue
|
|
189
|
+
return _Distribution(manifest.parent, canonicalize_name(name), frozenset(names), workspace)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _workspace(root: Path, document: dict[object, object]) -> _Workspace | None:
|
|
193
|
+
value: object = document
|
|
194
|
+
for key in ("tool", "uv", "workspace"):
|
|
195
|
+
if not _is_mapping(value):
|
|
196
|
+
return None
|
|
197
|
+
value = value.get(key)
|
|
198
|
+
if not _is_mapping(value):
|
|
199
|
+
return None
|
|
200
|
+
members = _patterns(value.get("members"))
|
|
201
|
+
excludes = _patterns(value.get("exclude", []))
|
|
202
|
+
if members is None or excludes is None:
|
|
203
|
+
return None
|
|
204
|
+
return _Workspace(root, members, excludes)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _patterns(value: object) -> tuple[str, ...] | None:
|
|
208
|
+
if not _is_list(value) or any(not isinstance(item, str) for item in value):
|
|
209
|
+
return None
|
|
210
|
+
return tuple(item for item in value if isinstance(item, str))
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _files(root: Path, pattern: str) -> list[Path]:
|
|
214
|
+
found: list[Path] = []
|
|
215
|
+
try:
|
|
216
|
+
for count, (directory, directories, filenames) in enumerate(root.walk()):
|
|
217
|
+
if count >= _MAX_DIRECTORIES or len(found) >= _MAX_FILES:
|
|
218
|
+
# A partial inventory cannot establish uniqueness.
|
|
219
|
+
return []
|
|
220
|
+
directories[:] = sorted(name for name in directories if not name.startswith(".") and name not in _SKIP)
|
|
221
|
+
found.extend(directory / name for name in sorted(filenames) if Path(name).match(pattern))
|
|
222
|
+
if len(found) > _MAX_FILES:
|
|
223
|
+
return []
|
|
224
|
+
except OSError:
|
|
225
|
+
return []
|
|
226
|
+
return found
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _sources(root: Path) -> dict[str, NominalSource]:
|
|
230
|
+
sources: dict[str, NominalSource] = {}
|
|
231
|
+
ambiguous: set[str] = set()
|
|
232
|
+
total_bytes = 0
|
|
233
|
+
for path in _files(root, "*.py"):
|
|
234
|
+
try:
|
|
235
|
+
total_bytes += path.stat().st_size
|
|
236
|
+
except OSError:
|
|
237
|
+
continue
|
|
238
|
+
if total_bytes > _MAX_TOTAL_BYTES:
|
|
239
|
+
return {}
|
|
240
|
+
module = _module_name(root, path)
|
|
241
|
+
if not module:
|
|
242
|
+
continue
|
|
243
|
+
source = _source(path)
|
|
244
|
+
if source is None:
|
|
245
|
+
continue
|
|
246
|
+
if module in sources:
|
|
247
|
+
ambiguous.add(module)
|
|
248
|
+
sources[module] = source
|
|
249
|
+
return {module: source for module, source in sources.items() if module not in ambiguous}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _module_name(root: Path, path: Path) -> str | None:
|
|
253
|
+
# A nested manifest owns its sources even when it uses the same namespace.
|
|
254
|
+
if any(
|
|
255
|
+
(parent / "pyproject.toml").is_file() for parent in path.parents if parent != root and root in parent.parents
|
|
256
|
+
):
|
|
257
|
+
return None
|
|
258
|
+
relative = path.relative_to(root)
|
|
259
|
+
parts = relative.parts[1:] if relative.parts[0] == "src" else relative.parts
|
|
260
|
+
return ".".join(parts[:-1] if parts[-1] == "__init__.py" else (*parts[:-1], path.stem)) or None
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _source(path: Path) -> NominalSource | None:
|
|
264
|
+
try:
|
|
265
|
+
if path.is_symlink() or path.stat().st_size > _MAX_BYTES:
|
|
266
|
+
return None
|
|
267
|
+
text = path.read_text(encoding="utf-8")
|
|
268
|
+
if is_generated(path, text):
|
|
269
|
+
return None
|
|
270
|
+
except OSError, UnicodeError:
|
|
271
|
+
return None
|
|
272
|
+
return NominalSource(path, text)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _is_mapping(value: object) -> TypeGuard[dict[object, object]]:
|
|
276
|
+
return isinstance(value, dict)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _is_list(value: object) -> TypeGuard[list[object]]:
|
|
280
|
+
return isinstance(value, list)
|
|
@@ -75,6 +75,7 @@ from sarj_python_lint.rules.no_secret_in_log import NoSecretInLog
|
|
|
75
75
|
from sarj_python_lint.rules.no_select_star import NoSelectStar
|
|
76
76
|
from sarj_python_lint.rules.no_service_behavior_in_settings import NoServiceBehaviorInSettings
|
|
77
77
|
from sarj_python_lint.rules.no_string_concat_in_loop import NoStringConcatInLoop
|
|
78
|
+
from sarj_python_lint.rules.no_swallowed_asyncio_cancellation import NoSwallowedAsyncioCancellation
|
|
78
79
|
from sarj_python_lint.rules.no_tautological_expect import NoTautologicalExpect
|
|
79
80
|
from sarj_python_lint.rules.no_typed_doc_sections import NoTypedDocSections
|
|
80
81
|
from sarj_python_lint.rules.no_unique_violation_message_match import (
|
|
@@ -181,6 +182,7 @@ if TYPE_CHECKING:
|
|
|
181
182
|
|
|
182
183
|
REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
183
184
|
{
|
|
185
|
+
NoSwallowedAsyncioCancellation.id: NoSwallowedAsyncioCancellation,
|
|
184
186
|
AsyncMockCallWithoutAwaitAssertion.id: AsyncMockCallWithoutAwaitAssertion,
|
|
185
187
|
ComplexPostgresQueryRequiresArchitectureReview.id: ComplexPostgresQueryRequiresArchitectureReview,
|
|
186
188
|
ExcessiveCommentary.id: ExcessiveCommentary,
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from itertools import pairwise
|
|
5
|
+
from pathlib import PurePosixPath
|
|
6
|
+
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
7
|
+
|
|
8
|
+
from sarj_python_lint.rule_base import (
|
|
9
|
+
AutofixPolicy,
|
|
10
|
+
Diagnostic,
|
|
11
|
+
ExampleFile,
|
|
12
|
+
ExampleOutcome,
|
|
13
|
+
Rule,
|
|
14
|
+
RuleCategory,
|
|
15
|
+
RuleDocumentation,
|
|
16
|
+
RuleExample,
|
|
17
|
+
Severity,
|
|
18
|
+
is_suppressed,
|
|
19
|
+
)
|
|
20
|
+
from sarj_python_lint.rules._ast_index import nodes
|
|
21
|
+
from sarj_python_lint.rules._paths import is_test_path, is_test_support_path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Iterator
|
|
26
|
+
|
|
27
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
28
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_ASYNCIO = frozenset({"asyncio", "asyncio.exceptions"})
|
|
32
|
+
_SIMPLE_STATEMENTS = (ast.Expr, ast.Assign, ast.AnnAssign, ast.AugAssign, ast.Delete, ast.Pass)
|
|
33
|
+
_MESSAGE = (
|
|
34
|
+
"CancelledError handler completes normally; re-raise caller cancellation after cleanup, "
|
|
35
|
+
"or guard intentional child cancellation with the current task's cancellation state."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@final
|
|
40
|
+
class NoSwallowedAsyncioCancellation(Rule):
|
|
41
|
+
id = "no-swallowed-asyncio-cancellation"
|
|
42
|
+
code = "SARJ458"
|
|
43
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
44
|
+
default_level=Severity.WARNING,
|
|
45
|
+
summary="Preserve cancellation when an async operation is cancelled.",
|
|
46
|
+
rationale=(
|
|
47
|
+
"Returning or falling through after catching CancelledError can make cancelled work look successful "
|
|
48
|
+
"and interfere with task groups and timeout shutdown. Child cancellation must be distinguished from "
|
|
49
|
+
"cancellation of the caller."
|
|
50
|
+
),
|
|
51
|
+
remediation=(
|
|
52
|
+
"Re-raise after cleanup. If child cancellation is an expected result, first propagate cancellation "
|
|
53
|
+
"when asyncio.current_task().cancelling() is nonzero. Use an exact suppression for a documented "
|
|
54
|
+
"boundary that deliberately consumes cancellation."
|
|
55
|
+
),
|
|
56
|
+
category=RuleCategory.CORRECTNESS,
|
|
57
|
+
autofix=AutofixPolicy.NONE,
|
|
58
|
+
limitations=(
|
|
59
|
+
"Only explicit, import-proven asyncio.CancelledError handlers in async functions are inspected; broad handlers, contextlib.suppress, tests, support fixtures, and generated files are excluded.",
|
|
60
|
+
"Only straight-line handlers with a return or normal fallthrough are reported. Compound handlers, re-raising, and translated exceptions are excluded. Any attribute call named uncancel is conservatively excluded without proving its receiver, so unrelated methods can hide a finding.",
|
|
61
|
+
"A single-name task created by asyncio.create_task, immediately cancelled before a try that only awaits it, is intentionally excluded as child cleanup; this does not prove concurrent caller cancellation cannot occur.",
|
|
62
|
+
"Try statements with control-transferring or compound finalizers are excluded, including handlers nested beneath them. Runtime helper effects and interprocedural task ownership are not inferred.",
|
|
63
|
+
),
|
|
64
|
+
examples=(
|
|
65
|
+
RuleExample(
|
|
66
|
+
example_id="cancelled-operation-returns-success",
|
|
67
|
+
title="Cancellation becomes a successful return",
|
|
68
|
+
outcome=ExampleOutcome.MATCH,
|
|
69
|
+
files=(
|
|
70
|
+
ExampleFile.python(
|
|
71
|
+
"worker.py",
|
|
72
|
+
"import asyncio\n\nasync def worker():\n try:\n await asyncio.sleep(10)\n except asyncio.CancelledError:\n return None\n",
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
focus_path=PurePosixPath("worker.py"),
|
|
76
|
+
expected_count=1,
|
|
77
|
+
public=True,
|
|
78
|
+
),
|
|
79
|
+
RuleExample(
|
|
80
|
+
example_id="cancelled-operation-reraises",
|
|
81
|
+
title="Cleanup preserves cancellation",
|
|
82
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
83
|
+
files=(
|
|
84
|
+
ExampleFile.python(
|
|
85
|
+
"worker.py",
|
|
86
|
+
"import asyncio\n\nasync def worker():\n try:\n await asyncio.sleep(10)\n except asyncio.CancelledError:\n raise\n",
|
|
87
|
+
),
|
|
88
|
+
),
|
|
89
|
+
focus_path=PurePosixPath("worker.py"),
|
|
90
|
+
expected_count=0,
|
|
91
|
+
public=True,
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
description = documentation.summary
|
|
96
|
+
|
|
97
|
+
@override
|
|
98
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
99
|
+
if "CancelledError" not in context.source:
|
|
100
|
+
return []
|
|
101
|
+
if context.generated or is_test_path(context.path) or is_test_support_path(context.path):
|
|
102
|
+
return []
|
|
103
|
+
tree = context.tree
|
|
104
|
+
if tree is None:
|
|
105
|
+
return []
|
|
106
|
+
diagnostics: list[Diagnostic] = []
|
|
107
|
+
for function in nodes(tree, ast.AsyncFunctionDef, index=context.node_index):
|
|
108
|
+
diagnostics.extend(_function_diagnostics(function, context, self.code))
|
|
109
|
+
return sorted(diagnostics, key=lambda item: (item.line, item.col))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _function_diagnostics(function: ast.AsyncFunctionDef, context: PythonFileContext, code: str) -> list[Diagnostic]:
|
|
113
|
+
local = tuple(_local_nodes(function))
|
|
114
|
+
children = _owned_tasks(local, context.imports)
|
|
115
|
+
previous = _preceding_statements(local)
|
|
116
|
+
diagnostics: list[Diagnostic] = []
|
|
117
|
+
for node in local:
|
|
118
|
+
if not isinstance(node, ast.Try) or _owned_child_cleanup(node, previous.get(id(node)), children):
|
|
119
|
+
continue
|
|
120
|
+
for handler in node.handlers:
|
|
121
|
+
if not _catches_cancellation(handler.type, context.imports) or not _normal_exit(handler.body):
|
|
122
|
+
continue
|
|
123
|
+
if is_suppressed(context.source_lines, handler.lineno, code):
|
|
124
|
+
continue
|
|
125
|
+
diagnostics.append(
|
|
126
|
+
Diagnostic(
|
|
127
|
+
path=context.path,
|
|
128
|
+
line=handler.lineno,
|
|
129
|
+
col=handler.col_offset + 1,
|
|
130
|
+
code=code,
|
|
131
|
+
message=_MESSAGE,
|
|
132
|
+
severity=Severity.WARNING,
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
return diagnostics
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _local_nodes(function: ast.AsyncFunctionDef) -> Iterator[ast.AST]:
|
|
139
|
+
stack: list[ast.AST] = list(reversed(function.body))
|
|
140
|
+
yield function
|
|
141
|
+
while stack:
|
|
142
|
+
node = stack.pop()
|
|
143
|
+
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Lambda):
|
|
144
|
+
continue
|
|
145
|
+
if isinstance(node, ast.Try | ast.TryStar) and any(
|
|
146
|
+
not isinstance(statement, _SIMPLE_STATEMENTS) for statement in node.finalbody
|
|
147
|
+
):
|
|
148
|
+
continue
|
|
149
|
+
yield node
|
|
150
|
+
stack.extend(reversed(list(ast.iter_child_nodes(node))))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _catches_cancellation(exception: ast.expr | None, imports: ImportIndex) -> bool:
|
|
154
|
+
if isinstance(exception, ast.Tuple):
|
|
155
|
+
return any(_catches_cancellation(item, imports) for item in exception.elts)
|
|
156
|
+
return exception is not None and imports.resolves(exception, sources=_ASYNCIO, symbol="CancelledError")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _normal_exit(statements: list[ast.stmt]) -> bool:
|
|
160
|
+
for statement in statements:
|
|
161
|
+
if any(
|
|
162
|
+
isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "uncancel"
|
|
163
|
+
for node in ast.walk(statement)
|
|
164
|
+
):
|
|
165
|
+
return False
|
|
166
|
+
if isinstance(statement, ast.Return):
|
|
167
|
+
return True
|
|
168
|
+
if not isinstance(statement, _SIMPLE_STATEMENTS):
|
|
169
|
+
return False
|
|
170
|
+
return True
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _owned_tasks(local: tuple[ast.AST, ...], imports: ImportIndex) -> frozenset[str]:
|
|
174
|
+
bindings: dict[str, list[ast.AST]] = {}
|
|
175
|
+
for node in local:
|
|
176
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store | ast.Del):
|
|
177
|
+
bindings.setdefault(node.id, []).append(node)
|
|
178
|
+
if (
|
|
179
|
+
isinstance(node, ast.Attribute)
|
|
180
|
+
and isinstance(node.ctx, ast.Store | ast.Del)
|
|
181
|
+
and isinstance(node.value, ast.Name)
|
|
182
|
+
):
|
|
183
|
+
bindings.setdefault(node.value.id, []).append(node)
|
|
184
|
+
return frozenset(
|
|
185
|
+
node.targets[0].id
|
|
186
|
+
for node in local
|
|
187
|
+
if isinstance(node, ast.Assign)
|
|
188
|
+
and len(node.targets) == 1
|
|
189
|
+
and isinstance(node.targets[0], ast.Name)
|
|
190
|
+
and len(bindings[node.targets[0].id]) == 1
|
|
191
|
+
and isinstance(node.value, ast.Call)
|
|
192
|
+
and imports.resolves(node.value.func, sources=frozenset({"asyncio"}), symbol="create_task")
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _preceding_statements(local: tuple[ast.AST, ...]) -> dict[int, ast.stmt]:
|
|
197
|
+
previous: dict[int, ast.stmt] = {}
|
|
198
|
+
for node in local:
|
|
199
|
+
for suite in _statement_suites(node):
|
|
200
|
+
for first, second in pairwise(suite):
|
|
201
|
+
previous[id(second)] = first
|
|
202
|
+
return previous
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _owned_child_cleanup(node: ast.Try, previous: ast.stmt | None, children: frozenset[str]) -> bool:
|
|
206
|
+
if len(node.body) != 1:
|
|
207
|
+
return False
|
|
208
|
+
match node.body[0], previous:
|
|
209
|
+
case (
|
|
210
|
+
ast.Expr(value=ast.Await(value=ast.Name(id=awaited))),
|
|
211
|
+
ast.Expr(value=ast.Call(func=ast.Attribute(value=ast.Name(id=cancelled), attr="cancel"))),
|
|
212
|
+
):
|
|
213
|
+
return awaited == cancelled and awaited in children
|
|
214
|
+
case _:
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _statement_suites(node: ast.AST) -> tuple[list[ast.stmt], ...]:
|
|
219
|
+
match node:
|
|
220
|
+
case ast.Try() | ast.TryStar():
|
|
221
|
+
return node.body, node.orelse, node.finalbody
|
|
222
|
+
case ast.If() | ast.For() | ast.AsyncFor() | ast.While():
|
|
223
|
+
return node.body, node.orelse
|
|
224
|
+
case ast.AsyncFunctionDef() | ast.With() | ast.AsyncWith() | ast.ExceptHandler() | ast.match_case():
|
|
225
|
+
return (node.body,)
|
|
226
|
+
case _:
|
|
227
|
+
return ()
|