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