sarj-python-lint 1.8.3__tar.gz → 1.9.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.8.3 → sarj_python_lint-1.9.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_registry.py +2 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +10 -3
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +26 -26
- sarj_python_lint-1.9.0/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +276 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +191 -45
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/.gitignore +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/LICENSE +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/README.md +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/discourage_nullable_constructor_parameters.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.8.3 → sarj_python_lint-1.9.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.9.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/
|
|
@@ -60,6 +60,7 @@ from sarj_python_lint.rules.no_hidden_constructor_fallback import (
|
|
|
60
60
|
)
|
|
61
61
|
from sarj_python_lint.rules.no_invalid_argument_name_suppression import NoInvalidArgumentNameSuppression
|
|
62
62
|
from sarj_python_lint.rules.no_nested_pydantic_field_validator import NoNestedPydanticFieldValidator
|
|
63
|
+
from sarj_python_lint.rules.no_nullable_dependency_fallback import NoNullableDependencyFallback
|
|
63
64
|
from sarj_python_lint.rules.no_offset_pagination import NoOffsetPagination
|
|
64
65
|
from sarj_python_lint.rules.no_positional_psycopg_row_escape import NoPositionalPsycopgRowEscape
|
|
65
66
|
from sarj_python_lint.rules.no_psycopg_execution_outside_injected_owner import (
|
|
@@ -254,6 +255,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
254
255
|
NoRandomUuidInSql.id: NoRandomUuidInSql,
|
|
255
256
|
NoRedundantModuleAliasExports.id: NoRedundantModuleAliasExports,
|
|
256
257
|
NoHiddenConstructorFallback.id: NoHiddenConstructorFallback,
|
|
258
|
+
NoNullableDependencyFallback.id: NoNullableDependencyFallback,
|
|
257
259
|
NoFileLevelEscapeHatchSuppression.id: NoFileLevelEscapeHatchSuppression,
|
|
258
260
|
NoInvalidArgumentNameSuppression.id: NoInvalidArgumentNameSuppression,
|
|
259
261
|
NoFastapiOnEvent.id: NoFastapiOnEvent,
|
|
@@ -15,14 +15,19 @@ from sarj_python_lint.rule_base import (
|
|
|
15
15
|
Severity,
|
|
16
16
|
)
|
|
17
17
|
from sarj_python_lint.rules._paths import is_test_path
|
|
18
|
-
from sarj_python_lint.rules.no_raw_source_text_test_oracle import
|
|
18
|
+
from sarj_python_lint.rules.no_raw_source_text_test_oracle import (
|
|
19
|
+
IAC_JSON_SOURCE_SUFFIXES,
|
|
20
|
+
FunctionAnalyzer,
|
|
21
|
+
module_source_path_tuples,
|
|
22
|
+
top_level_test_functions,
|
|
23
|
+
)
|
|
19
24
|
|
|
20
25
|
|
|
21
26
|
if TYPE_CHECKING:
|
|
22
27
|
from sarj_python_lint._file_context import PythonFileContext
|
|
23
28
|
|
|
24
29
|
|
|
25
|
-
IAC_SOURCE_SUFFIXES = (".hcl", ".tf", ".
|
|
30
|
+
IAC_SOURCE_SUFFIXES = (".hcl", ".tf", ".tfvars", ".tftest.hcl", *IAC_JSON_SOURCE_SUFFIXES)
|
|
26
31
|
|
|
27
32
|
|
|
28
33
|
@final
|
|
@@ -42,7 +47,7 @@ class IacSourceCoupledTest(Rule):
|
|
|
42
47
|
),
|
|
43
48
|
category=RuleCategory.TESTING,
|
|
44
49
|
limitations=(
|
|
45
|
-
"The rule follows local aliases, path collections, context-managed reads, and
|
|
50
|
+
"The rule follows local aliases, path collections, context-managed reads, common normalization, and direct comprehension reads unpacked from stable module-level path tuples; interprocedural flows remain unreported.",
|
|
46
51
|
"Files produced beneath recognized temporary-directory fixtures are generated outputs and remain unreported.",
|
|
47
52
|
"The Python detector currently owns Terraform and HCL suffixes; YAML remains with the general source-coupled rule.",
|
|
48
53
|
"Fixture, golden, and snapshot paths are treated as deliberate representation contracts; other packaging, formatter, and compatibility contracts require an exact suppression.",
|
|
@@ -93,11 +98,13 @@ class IacSourceCoupledTest(Rule):
|
|
|
93
98
|
return []
|
|
94
99
|
imports = context.module_imports
|
|
95
100
|
source_lines = context.source_lines
|
|
101
|
+
module_tuples = module_source_path_tuples(tree, imports, IAC_SOURCE_SUFFIXES)
|
|
96
102
|
assertions = [
|
|
97
103
|
assertion
|
|
98
104
|
for function, unittest_style in top_level_test_functions(tree, imports)
|
|
99
105
|
for assertion in FunctionAnalyzer(
|
|
100
106
|
IAC_SOURCE_SUFFIXES,
|
|
107
|
+
module_path_tuples=module_tuples,
|
|
101
108
|
imports=imports,
|
|
102
109
|
suppression_code=self.code,
|
|
103
110
|
suppression_lines=source_lines,
|
|
@@ -74,7 +74,7 @@ class _CanonicalSymbol(NamedTuple):
|
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
@dataclass(slots=True)
|
|
77
|
-
class
|
|
77
|
+
class RuntimeConfigFacts:
|
|
78
78
|
modules: dict[Path, ast.Module] = field(default_factory=dict)
|
|
79
79
|
composition_calls: dict[tuple[Path, str, str], bool] = field(default_factory=dict)
|
|
80
80
|
canonical_symbols: dict[tuple[Path, str, str], _CanonicalSymbol] = field(default_factory=dict)
|
|
@@ -82,7 +82,7 @@ class _ConstructorFacts:
|
|
|
82
82
|
|
|
83
83
|
@final
|
|
84
84
|
class NoHiddenConstructorFallback(Rule):
|
|
85
|
-
_constructor_facts:
|
|
85
|
+
_constructor_facts: RuntimeConfigFacts | None = None
|
|
86
86
|
id = "no-hidden-constructor-fallback"
|
|
87
87
|
code = "SARJ095"
|
|
88
88
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
@@ -155,7 +155,7 @@ class NoHiddenConstructorFallback(Rule):
|
|
|
155
155
|
@override
|
|
156
156
|
def prepare_session(self, session: AnalysisSession) -> None:
|
|
157
157
|
super().prepare_session(session)
|
|
158
|
-
self._constructor_facts =
|
|
158
|
+
self._constructor_facts = RuntimeConfigFacts()
|
|
159
159
|
|
|
160
160
|
@override
|
|
161
161
|
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
@@ -169,8 +169,8 @@ class NoHiddenConstructorFallback(Rule):
|
|
|
169
169
|
first_party = context.session.first_party
|
|
170
170
|
facts = self._constructor_facts if self._analysis_session is context.session else None
|
|
171
171
|
if facts is None:
|
|
172
|
-
facts =
|
|
173
|
-
resolver =
|
|
172
|
+
facts = RuntimeConfigFacts()
|
|
173
|
+
resolver = RuntimeConfigResolver(path, tree, first_party, facts)
|
|
174
174
|
diagnostics: list[Diagnostic] = []
|
|
175
175
|
|
|
176
176
|
def collect_hidden_constructors() -> None:
|
|
@@ -210,7 +210,7 @@ def _is_descriptor(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
|
210
210
|
|
|
211
211
|
|
|
212
212
|
def _default_scope(owner: ast.ClassDef, init: ast.FunctionDef | ast.AsyncFunctionDef) -> set[str]:
|
|
213
|
-
collector =
|
|
213
|
+
collector = LocalBindingCollector()
|
|
214
214
|
for statement in owner.body:
|
|
215
215
|
if statement is init:
|
|
216
216
|
break
|
|
@@ -220,7 +220,7 @@ def _default_scope(owner: ast.ClassDef, init: ast.FunctionDef | ast.AsyncFunctio
|
|
|
220
220
|
|
|
221
221
|
def _hidden_parameters(
|
|
222
222
|
init: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
223
|
-
resolver:
|
|
223
|
+
resolver: RuntimeConfigResolver,
|
|
224
224
|
default_shadowed: set[str] | None = None,
|
|
225
225
|
) -> list[_HiddenParameter]:
|
|
226
226
|
positional = (*init.args.posonlyargs, *init.args.args)
|
|
@@ -247,7 +247,7 @@ def _hidden_parameters(
|
|
|
247
247
|
# Python decides every local binding for the whole function before it
|
|
248
248
|
# executes. Seed the resolver with that complete scope so a branch-local
|
|
249
249
|
# `settings = ...` cannot be mistaken for the imported settings object.
|
|
250
|
-
shadowed = set(
|
|
250
|
+
shadowed = set(scope_bindings(init))
|
|
251
251
|
receiver = positional[0].arg if positional else None
|
|
252
252
|
for statement in init.body:
|
|
253
253
|
available = candidates.keys() - rebound
|
|
@@ -263,7 +263,7 @@ def _hidden_parameters(
|
|
|
263
263
|
def _statement_fallbacks(
|
|
264
264
|
statement: ast.stmt,
|
|
265
265
|
candidates: set[str],
|
|
266
|
-
resolver:
|
|
266
|
+
resolver: RuntimeConfigResolver,
|
|
267
267
|
shadowed: set[str],
|
|
268
268
|
receiver: str | None,
|
|
269
269
|
) -> dict[str, bool]:
|
|
@@ -291,7 +291,7 @@ def _statement_fallbacks(
|
|
|
291
291
|
def _expression_fallbacks(
|
|
292
292
|
expression: ast.expr,
|
|
293
293
|
candidates: set[str],
|
|
294
|
-
resolver:
|
|
294
|
+
resolver: RuntimeConfigResolver,
|
|
295
295
|
shadowed: set[str],
|
|
296
296
|
) -> dict[str, bool]:
|
|
297
297
|
if isinstance(expression, ast.BoolOp) and isinstance(expression.op, ast.Or):
|
|
@@ -386,13 +386,13 @@ def _is_fallback_target(expression: ast.expr, parameter: str, receiver: str | No
|
|
|
386
386
|
|
|
387
387
|
|
|
388
388
|
@final
|
|
389
|
-
class
|
|
389
|
+
class RuntimeConfigResolver:
|
|
390
390
|
def __init__(
|
|
391
391
|
self,
|
|
392
392
|
path: Path,
|
|
393
393
|
tree: ast.Module,
|
|
394
394
|
first_party: FirstPartyFacts,
|
|
395
|
-
facts:
|
|
395
|
+
facts: RuntimeConfigFacts,
|
|
396
396
|
) -> None:
|
|
397
397
|
self._path = path
|
|
398
398
|
self._tree = tree
|
|
@@ -629,7 +629,7 @@ def _module_path(module: str, root: Path | None) -> Path | None:
|
|
|
629
629
|
return next((candidate for candidate in candidates if candidate.is_file()), None)
|
|
630
630
|
|
|
631
631
|
|
|
632
|
-
def _read_module(path: Path, facts:
|
|
632
|
+
def _read_module(path: Path, facts: RuntimeConfigFacts) -> ast.Module:
|
|
633
633
|
cached = facts.modules.get(path)
|
|
634
634
|
if cached is not None:
|
|
635
635
|
return cached
|
|
@@ -650,7 +650,7 @@ def _has_composition_call(
|
|
|
650
650
|
path: Path,
|
|
651
651
|
class_name: str,
|
|
652
652
|
first_party: FirstPartyFacts,
|
|
653
|
-
facts:
|
|
653
|
+
facts: RuntimeConfigFacts,
|
|
654
654
|
) -> bool:
|
|
655
655
|
root = distribution_root(path, facts=first_party)
|
|
656
656
|
module = _module_name(path, root)
|
|
@@ -663,7 +663,7 @@ def _distribution_calls_class(
|
|
|
663
663
|
root: Path,
|
|
664
664
|
target_module: str,
|
|
665
665
|
class_name: str,
|
|
666
|
-
facts:
|
|
666
|
+
facts: RuntimeConfigFacts,
|
|
667
667
|
) -> bool:
|
|
668
668
|
key = (root, target_module, class_name)
|
|
669
669
|
if key in facts.composition_calls:
|
|
@@ -677,7 +677,7 @@ def _distribution_calls_class_uncached(
|
|
|
677
677
|
root: Path,
|
|
678
678
|
target_module: str,
|
|
679
679
|
class_name: str,
|
|
680
|
-
facts:
|
|
680
|
+
facts: RuntimeConfigFacts,
|
|
681
681
|
) -> bool:
|
|
682
682
|
skip_directories = _SCAN_SKIP_PARTS | _MIGRATION_PARTS | {"test", "tests"}
|
|
683
683
|
for directory, directory_names, file_names in os.walk(root):
|
|
@@ -692,7 +692,7 @@ def _distribution_calls_class_uncached(
|
|
|
692
692
|
|
|
693
693
|
|
|
694
694
|
def _candidate_calls_class(
|
|
695
|
-
candidate: Path, root: Path, target_module: str, class_name: str, facts:
|
|
695
|
+
candidate: Path, root: Path, target_module: str, class_name: str, facts: RuntimeConfigFacts
|
|
696
696
|
) -> bool:
|
|
697
697
|
if is_test_path(candidate):
|
|
698
698
|
return False
|
|
@@ -721,7 +721,7 @@ def _module_calls_class(
|
|
|
721
721
|
target_module: str,
|
|
722
722
|
*,
|
|
723
723
|
class_name: str,
|
|
724
|
-
facts:
|
|
724
|
+
facts: RuntimeConfigFacts,
|
|
725
725
|
) -> bool:
|
|
726
726
|
for node, shadowed in _calls_with_shadowing(tree):
|
|
727
727
|
parts = _attribute_parts(node.func)
|
|
@@ -746,7 +746,7 @@ def _canonical_symbol(
|
|
|
746
746
|
root: Path,
|
|
747
747
|
module: str,
|
|
748
748
|
symbol: str,
|
|
749
|
-
facts:
|
|
749
|
+
facts: RuntimeConfigFacts,
|
|
750
750
|
) -> _CanonicalSymbol:
|
|
751
751
|
key = (root, module, symbol)
|
|
752
752
|
cached = facts.canonical_symbols.get(key)
|
|
@@ -762,7 +762,7 @@ def _canonical_symbol_inner(
|
|
|
762
762
|
module: str,
|
|
763
763
|
symbol: str,
|
|
764
764
|
seen: frozenset[tuple[str, str]],
|
|
765
|
-
facts:
|
|
765
|
+
facts: RuntimeConfigFacts,
|
|
766
766
|
) -> _CanonicalSymbol:
|
|
767
767
|
key = (module, symbol)
|
|
768
768
|
if key in seen:
|
|
@@ -793,7 +793,7 @@ def _calls_with_shadowing(
|
|
|
793
793
|
for outer in outer_nodes:
|
|
794
794
|
yield from _calls_with_shadowing(outer, shadowed, nested_scope_base)
|
|
795
795
|
lexical_parent = nested_scope_base if nested_scope_base is not None else shadowed
|
|
796
|
-
yield from _calls_with_shadowing(body, lexical_parent |
|
|
796
|
+
yield from _calls_with_shadowing(body, lexical_parent | scope_bindings(node))
|
|
797
797
|
return
|
|
798
798
|
case ast.FunctionDef() | ast.AsyncFunctionDef():
|
|
799
799
|
yield from _function_calls(node, shadowed, nested_scope_base)
|
|
@@ -814,8 +814,8 @@ def _calls_with_shadowing(
|
|
|
814
814
|
yield from _calls_with_shadowing(child, shadowed, nested_scope_base)
|
|
815
815
|
|
|
816
816
|
|
|
817
|
-
def
|
|
818
|
-
collector =
|
|
817
|
+
def scope_bindings(node: ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda) -> frozenset[str]:
|
|
818
|
+
collector = LocalBindingCollector()
|
|
819
819
|
for argument in (*node.args.posonlyargs, *node.args.args, *node.args.kwonlyargs):
|
|
820
820
|
collector.names.add(argument.arg)
|
|
821
821
|
if node.args.vararg is not None:
|
|
@@ -829,13 +829,13 @@ def _scope_bindings(node: ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda) -
|
|
|
829
829
|
|
|
830
830
|
|
|
831
831
|
def _class_bindings(node: ast.ClassDef) -> frozenset[str]:
|
|
832
|
-
collector =
|
|
832
|
+
collector = LocalBindingCollector()
|
|
833
833
|
for statement in node.body:
|
|
834
834
|
collector.visit(statement)
|
|
835
835
|
return frozenset(collector.names)
|
|
836
836
|
|
|
837
837
|
|
|
838
|
-
class
|
|
838
|
+
class LocalBindingCollector(ast.NodeVisitor):
|
|
839
839
|
def __init__(self) -> None:
|
|
840
840
|
self.names: set[str] = set()
|
|
841
841
|
self.globals: set[str] = set()
|
|
@@ -928,7 +928,7 @@ def _function_calls(
|
|
|
928
928
|
for outer in outer_nodes:
|
|
929
929
|
yield from _calls_with_shadowing(outer, shadowed, nested_scope_base)
|
|
930
930
|
lexical_parent = nested_scope_base if nested_scope_base is not None else shadowed
|
|
931
|
-
local_shadowed = lexical_parent |
|
|
931
|
+
local_shadowed = lexical_parent | scope_bindings(node)
|
|
932
932
|
for statement in node.body:
|
|
933
933
|
yield from _calls_with_shadowing(statement, local_shadowed)
|
|
934
934
|
return
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
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
|
+
)
|
|
19
|
+
from sarj_python_lint.rules._paths import is_test_path, is_test_support_path
|
|
20
|
+
from sarj_python_lint.rules.no_hidden_constructor_fallback import (
|
|
21
|
+
LocalBindingCollector,
|
|
22
|
+
RuntimeConfigFacts,
|
|
23
|
+
RuntimeConfigResolver,
|
|
24
|
+
scope_bindings,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from collections.abc import Iterator
|
|
30
|
+
|
|
31
|
+
from sarj_python_lint._analysis_session import AnalysisSession
|
|
32
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_SCOPES = (
|
|
36
|
+
ast.FunctionDef,
|
|
37
|
+
ast.AsyncFunctionDef,
|
|
38
|
+
ast.ClassDef,
|
|
39
|
+
ast.Lambda,
|
|
40
|
+
ast.ListComp,
|
|
41
|
+
ast.SetComp,
|
|
42
|
+
ast.DictComp,
|
|
43
|
+
ast.GeneratorExp,
|
|
44
|
+
)
|
|
45
|
+
_FALLBACK_VALUES = 2
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class _ScopeUsage:
|
|
50
|
+
parents: dict[ast.AST, ast.AST]
|
|
51
|
+
called: set[str]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@final
|
|
55
|
+
class NoNullableDependencyFallback(Rule):
|
|
56
|
+
_facts: RuntimeConfigFacts | None = None
|
|
57
|
+
id = "no-nullable-dependency-fallback"
|
|
58
|
+
code = "SARJ469"
|
|
59
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
60
|
+
default_level=Severity.WARNING,
|
|
61
|
+
summary="Require concrete function dependencies instead of resolving a None default inside the function.",
|
|
62
|
+
rationale="A None dependency silently selects another implementation or ambient configuration, hiding wiring mistakes and adding unnecessary branches.",
|
|
63
|
+
remediation="Resolve the dependency at the call site and pass it explicitly; a concrete callable default is also valid when its lifetime is appropriate.",
|
|
64
|
+
category=RuleCategory.ARCHITECTURE,
|
|
65
|
+
autofix=AutofixPolicy.NONE,
|
|
66
|
+
limitations=(
|
|
67
|
+
"Warns for None-defaulted function parameters with a fallback used as a callable or a proven application-settings fallback; genuine absent state is allowed.",
|
|
68
|
+
"Constructors remain owned by SARJ095 and SARJ468. Decorated functions, tests, generated code, nested scope captures, non-callable object fallbacks, and interprocedural forwarding are excluded.",
|
|
69
|
+
"No autofix: removing explicit None acceptance changes the callable contract, and moving a fallback can change initialization timing or error handling.",
|
|
70
|
+
),
|
|
71
|
+
examples=(
|
|
72
|
+
RuleExample(
|
|
73
|
+
example_id="nullable-factory",
|
|
74
|
+
title="Omission silently selects an implementation",
|
|
75
|
+
outcome=ExampleOutcome.MATCH,
|
|
76
|
+
files=(
|
|
77
|
+
ExampleFile.python(
|
|
78
|
+
"app/service.py",
|
|
79
|
+
"def run(factory: Factory | None = None):\n return (factory or DefaultFactory)()\n",
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
focus_path=PurePosixPath("app/service.py"),
|
|
83
|
+
expected_count=1,
|
|
84
|
+
public=True,
|
|
85
|
+
),
|
|
86
|
+
RuleExample(
|
|
87
|
+
example_id="concrete-factory",
|
|
88
|
+
title="Caller supplies a concrete dependency",
|
|
89
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
90
|
+
files=(ExampleFile.python("app/service.py", "def run(factory: Factory):\n return factory()\n"),),
|
|
91
|
+
focus_path=PurePosixPath("app/service.py"),
|
|
92
|
+
expected_count=0,
|
|
93
|
+
public=True,
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
description = documentation.summary
|
|
98
|
+
|
|
99
|
+
@override
|
|
100
|
+
def prepare_session(self, session: AnalysisSession) -> None:
|
|
101
|
+
super().prepare_session(session)
|
|
102
|
+
self._facts = RuntimeConfigFacts()
|
|
103
|
+
|
|
104
|
+
@override
|
|
105
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
106
|
+
if (
|
|
107
|
+
context.tree is None
|
|
108
|
+
or context.generated
|
|
109
|
+
or is_test_path(context.path)
|
|
110
|
+
or is_test_support_path(context.path)
|
|
111
|
+
):
|
|
112
|
+
return []
|
|
113
|
+
facts = self._facts if self._analysis_session is context.session else None
|
|
114
|
+
resolver = RuntimeConfigResolver(
|
|
115
|
+
context.path, context.tree, context.session.first_party, facts or RuntimeConfigFacts()
|
|
116
|
+
)
|
|
117
|
+
findings: list[Diagnostic] = []
|
|
118
|
+
for function in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef):
|
|
119
|
+
if function.name == "__init__" or function.decorator_list:
|
|
120
|
+
continue
|
|
121
|
+
parameters = _none_default_parameters(function.args)
|
|
122
|
+
if not parameters:
|
|
123
|
+
continue
|
|
124
|
+
hidden = _hidden_dependencies(function, parameters, resolver)
|
|
125
|
+
findings.extend(
|
|
126
|
+
Diagnostic(
|
|
127
|
+
path=context.path,
|
|
128
|
+
line=argument.lineno,
|
|
129
|
+
col=argument.col_offset + 1,
|
|
130
|
+
code=self.code,
|
|
131
|
+
message=f"Dependency `{argument.arg}` defaults to None and selects a fallback; pass a concrete dependency from the caller or use a concrete callable default.",
|
|
132
|
+
severity=Severity.WARNING,
|
|
133
|
+
)
|
|
134
|
+
for argument in parameters.values()
|
|
135
|
+
if argument.arg in hidden
|
|
136
|
+
)
|
|
137
|
+
return sorted(findings, key=lambda finding: (finding.line, finding.col))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _none_default_parameters(arguments: ast.arguments) -> dict[str, ast.arg]:
|
|
141
|
+
positional = (*arguments.posonlyargs, *arguments.args)
|
|
142
|
+
defaulted = positional[-len(arguments.defaults) :] if arguments.defaults else ()
|
|
143
|
+
return {
|
|
144
|
+
argument.arg: argument
|
|
145
|
+
for argument, default in (
|
|
146
|
+
*zip(defaulted, arguments.defaults, strict=True),
|
|
147
|
+
*zip(arguments.kwonlyargs, arguments.kw_defaults, strict=True),
|
|
148
|
+
)
|
|
149
|
+
if _is_none(default)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _hidden_dependencies(
|
|
154
|
+
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
155
|
+
parameters: dict[str, ast.arg],
|
|
156
|
+
resolver: RuntimeConfigResolver,
|
|
157
|
+
) -> set[str]:
|
|
158
|
+
usage = _scope_usage(function)
|
|
159
|
+
candidates = set(parameters)
|
|
160
|
+
hidden: set[str] = set()
|
|
161
|
+
shadowed = set(scope_bindings(function))
|
|
162
|
+
for statement in function.body:
|
|
163
|
+
guard = _guarded_fallback(statement, candidates)
|
|
164
|
+
bindings = LocalBindingCollector()
|
|
165
|
+
bindings.visit(statement)
|
|
166
|
+
# Branch/loop bindings are conservatively local; do not infer data flow.
|
|
167
|
+
if not isinstance(statement, (ast.Assign, ast.AnnAssign)) and guard is None:
|
|
168
|
+
candidates.difference_update(bindings.names)
|
|
169
|
+
hidden.update(
|
|
170
|
+
_expression_dependencies(
|
|
171
|
+
statement,
|
|
172
|
+
candidates=candidates,
|
|
173
|
+
resolver=resolver,
|
|
174
|
+
shadowed=shadowed,
|
|
175
|
+
usage=usage,
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
if guard is not None:
|
|
179
|
+
name, value = guard
|
|
180
|
+
if name in usage.called or _is_settings_fallback(value, resolver, shadowed):
|
|
181
|
+
hidden.add(name)
|
|
182
|
+
candidates.difference_update(bindings.names)
|
|
183
|
+
return hidden
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _scope_usage(function: ast.FunctionDef | ast.AsyncFunctionDef) -> _ScopeUsage:
|
|
187
|
+
nodes = [node for statement in function.body for node in _scope_nodes(statement)]
|
|
188
|
+
parents = {child: node for node in nodes for child in ast.iter_child_nodes(node)}
|
|
189
|
+
called = {node.func.id for node in nodes if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)}
|
|
190
|
+
return _ScopeUsage(parents=parents, called=called)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _expression_dependencies(
|
|
194
|
+
statement: ast.stmt,
|
|
195
|
+
*,
|
|
196
|
+
candidates: set[str],
|
|
197
|
+
resolver: RuntimeConfigResolver,
|
|
198
|
+
shadowed: set[str],
|
|
199
|
+
usage: _ScopeUsage,
|
|
200
|
+
) -> set[str]:
|
|
201
|
+
hidden: set[str] = set()
|
|
202
|
+
for node in _scope_nodes(statement):
|
|
203
|
+
fallback = _fallback(node, candidates)
|
|
204
|
+
if fallback is None:
|
|
205
|
+
continue
|
|
206
|
+
name, value = fallback
|
|
207
|
+
if _is_settings_fallback(value, resolver, shadowed) or _used_as_dependency(node, usage):
|
|
208
|
+
hidden.add(name)
|
|
209
|
+
return hidden
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _is_settings_fallback(value: ast.expr, resolver: RuntimeConfigResolver, shadowed: set[str]) -> bool:
|
|
213
|
+
target = value.func if isinstance(value, ast.Call) else value
|
|
214
|
+
return resolver.is_runtime_config(target, shadowed)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _scope_nodes(statement: ast.AST) -> Iterator[ast.AST]:
|
|
218
|
+
if isinstance(statement, _SCOPES):
|
|
219
|
+
return
|
|
220
|
+
yield statement
|
|
221
|
+
for child in ast.iter_child_nodes(statement):
|
|
222
|
+
yield from _scope_nodes(child)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _guarded_fallback(statement: ast.stmt, candidates: set[str]) -> tuple[str, ast.expr] | None:
|
|
226
|
+
if not isinstance(statement, ast.If) or statement.orelse or len(statement.body) != 1:
|
|
227
|
+
return None
|
|
228
|
+
name = _none_guard(statement.test)
|
|
229
|
+
assignment = statement.body[0]
|
|
230
|
+
if name not in candidates or not isinstance(assignment, ast.Assign) or len(assignment.targets) != 1:
|
|
231
|
+
return None
|
|
232
|
+
target = assignment.targets[0]
|
|
233
|
+
if isinstance(target, ast.Name) and target.id == name:
|
|
234
|
+
return name, assignment.value
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _fallback(node: ast.AST, candidates: set[str]) -> tuple[str, ast.expr] | None:
|
|
239
|
+
if isinstance(node, ast.BoolOp) and isinstance(node.op, ast.Or) and len(node.values) == _FALLBACK_VALUES:
|
|
240
|
+
value, fallback = node.values
|
|
241
|
+
if isinstance(value, ast.Name) and value.id in candidates:
|
|
242
|
+
return value.id, fallback
|
|
243
|
+
if isinstance(node, ast.IfExp):
|
|
244
|
+
name = _none_guard(node.test)
|
|
245
|
+
value, fallback = node.orelse, node.body
|
|
246
|
+
if name is None:
|
|
247
|
+
name = _none_guard(node.test, inverse=True)
|
|
248
|
+
value, fallback = node.body, node.orelse
|
|
249
|
+
if name in candidates and isinstance(value, ast.Name) and value.id == name:
|
|
250
|
+
return name, fallback
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _none_guard(node: ast.AST, *, inverse: bool = False) -> str | None:
|
|
255
|
+
if not isinstance(node, ast.Compare) or len(node.ops) != 1 or len(node.comparators) != 1:
|
|
256
|
+
return None
|
|
257
|
+
if not isinstance(node.ops[0], ast.IsNot if inverse else ast.Is):
|
|
258
|
+
return None
|
|
259
|
+
for value, other in ((node.left, node.comparators[0]), (node.comparators[0], node.left)):
|
|
260
|
+
if isinstance(value, ast.Name) and _is_none(other):
|
|
261
|
+
return value.id
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _is_none(node: ast.AST | None) -> bool:
|
|
266
|
+
return isinstance(node, ast.Constant) and node.value is None
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _used_as_dependency(node: ast.AST, usage: _ScopeUsage) -> bool:
|
|
270
|
+
parent = usage.parents.get(node)
|
|
271
|
+
if isinstance(parent, ast.Call) and parent.func is node:
|
|
272
|
+
return True
|
|
273
|
+
if isinstance(parent, ast.Assign) and parent.value is node and len(parent.targets) == 1:
|
|
274
|
+
target = parent.targets[0]
|
|
275
|
+
return isinstance(target, ast.Name) and target.id in usage.called
|
|
276
|
+
return False
|