sarj-python-lint 1.16.0__tar.gz → 1.16.2__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.16.0 → sarj_python_lint-1.16.2}/PKG-INFO +1 -1
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/pyproject.toml +1 -1
- sarj_python_lint-1.16.2/src/sarj_python_lint/rules/discourage_nullable_constructor_parameters.py +171 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +123 -21
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_database_programmability.py +2 -2
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_injected_module_loader.py +38 -5
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +1 -1
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +21 -3
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_unused_value_marker.py +15 -1
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +22 -14
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +78 -10
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +35 -14
- sarj_python_lint-1.16.2/src/sarj_python_lint/rules/require_explicit_service_contract.py +579 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/require_precise_factory_signature.py +12 -2
- sarj_python_lint-1.16.2/src/sarj_python_lint/rules/require_public_dependency_contract.py +248 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/unused_test_factory_option.py +25 -101
- sarj_python_lint-1.16.0/src/sarj_python_lint/rules/discourage_nullable_constructor_parameters.py +0 -122
- sarj_python_lint-1.16.0/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -338
- sarj_python_lint-1.16.0/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -196
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/.gitignore +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/LICENSE +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/README.md +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.16.0 → sarj_python_lint-1.16.2}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 1.16.
|
|
3
|
+
Version: 1.16.2
|
|
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/
|
sarj_python_lint-1.16.2/src/sarj_python_lint/rules/discourage_nullable_constructor_parameters.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
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
|
+
)
|
|
18
|
+
from sarj_python_lint.rules._paths import is_test_path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_REJECTED = (
|
|
26
|
+
"class Service:\n"
|
|
27
|
+
" def __init__(self, store: Store | None = None) -> None:\n"
|
|
28
|
+
" if store is None:\n"
|
|
29
|
+
" raise ValueError('store is required')\n"
|
|
30
|
+
" self.store = store\n"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@final
|
|
35
|
+
class DiscourageNullableConstructorParameters(Rule):
|
|
36
|
+
id = "discourage-nullable-constructor-parameters"
|
|
37
|
+
code = "SARJ468"
|
|
38
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
39
|
+
default_level=Severity.WARNING,
|
|
40
|
+
summary="Make constructor inputs required when their None default is immediately rejected.",
|
|
41
|
+
rationale=(
|
|
42
|
+
"A constructor that immediately rejects its None default advertises omission even though it cannot "
|
|
43
|
+
"construct an instance from that default. Static typing permits the omitted call."
|
|
44
|
+
),
|
|
45
|
+
remediation=(
|
|
46
|
+
"Make the rejected input required and non-nullable. Keep deliberate optional state nullable; "
|
|
47
|
+
"no wrapper, interface, or alternate constructor is needed."
|
|
48
|
+
),
|
|
49
|
+
category=RuleCategory.ARCHITECTURE,
|
|
50
|
+
autofix=AutofixPolicy.NONE,
|
|
51
|
+
limitations=(
|
|
52
|
+
"Requires a PEP604 nullable parameter with a literal None default and immediate explicit rejection in an undecorated synchronous __init__ body.",
|
|
53
|
+
"Only a first executable `if parameter is None: raise ...` or `assert parameter is not None` is checked; docstrings and simple assignments preparing a raised error are allowed. Compound guards, later validation, fallbacks, and required nullable inputs are excluded.",
|
|
54
|
+
"An assertion expresses rejection in normal assertions-enabled execution; Python -O can remove it. No autofix changes a public constructor signature or error type.",
|
|
55
|
+
"Tests, generated code, decorated classes, and constructors with decorators are excluded. Nullable state that is stored or handled is valid.",
|
|
56
|
+
),
|
|
57
|
+
examples=(
|
|
58
|
+
RuleExample(
|
|
59
|
+
example_id="rejected-constructor-default",
|
|
60
|
+
title="The constructor immediately rejects its advertised default",
|
|
61
|
+
outcome=ExampleOutcome.MATCH,
|
|
62
|
+
files=(ExampleFile.python("app/service.py", _REJECTED),),
|
|
63
|
+
focus_path=PurePosixPath("app/service.py"),
|
|
64
|
+
expected_count=1,
|
|
65
|
+
public=True,
|
|
66
|
+
),
|
|
67
|
+
RuleExample(
|
|
68
|
+
example_id="required-constructor-input",
|
|
69
|
+
title="The signature requires the input that construction needs",
|
|
70
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
71
|
+
files=(ExampleFile.python("app/service.py", _REJECTED.replace("Store | None = None", "Store")),),
|
|
72
|
+
focus_path=PurePosixPath("app/service.py"),
|
|
73
|
+
expected_count=0,
|
|
74
|
+
public=True,
|
|
75
|
+
),
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
description = documentation.summary
|
|
79
|
+
|
|
80
|
+
@override
|
|
81
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
82
|
+
if context.generated or context.tree is None or is_test_path(context.path):
|
|
83
|
+
return []
|
|
84
|
+
findings: list[Diagnostic] = []
|
|
85
|
+
for owner in context.nodes(ast.ClassDef):
|
|
86
|
+
if owner.decorator_list:
|
|
87
|
+
continue
|
|
88
|
+
for method in owner.body:
|
|
89
|
+
if not isinstance(method, ast.FunctionDef) or method.name != "__init__" or method.decorator_list:
|
|
90
|
+
continue
|
|
91
|
+
parameter = _rejected_default(method)
|
|
92
|
+
if parameter is None:
|
|
93
|
+
continue
|
|
94
|
+
findings.append(
|
|
95
|
+
Diagnostic(
|
|
96
|
+
path=context.path,
|
|
97
|
+
line=parameter.lineno,
|
|
98
|
+
col=parameter.col_offset + 1,
|
|
99
|
+
code=self.code,
|
|
100
|
+
severity=Severity.WARNING,
|
|
101
|
+
message=f"Constructor input `{parameter.arg}` defaults to None but immediately rejects it; make this input required and non-nullable instead of adding an abstraction.",
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
return findings
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _rejected_default(method: ast.FunctionDef) -> ast.arg | None:
|
|
108
|
+
rejected = _immediately_rejected_parameter(method)
|
|
109
|
+
if rejected is None:
|
|
110
|
+
return None
|
|
111
|
+
positional = (*method.args.posonlyargs, *method.args.args)
|
|
112
|
+
defaulted = positional[-len(method.args.defaults) :] if method.args.defaults else ()
|
|
113
|
+
defaults = (
|
|
114
|
+
*zip(defaulted, method.args.defaults, strict=True),
|
|
115
|
+
*zip(method.args.kwonlyargs, method.args.kw_defaults, strict=True),
|
|
116
|
+
)
|
|
117
|
+
for parameter, default in defaults:
|
|
118
|
+
if parameter.arg != rejected or (positional and parameter is positional[0]):
|
|
119
|
+
continue
|
|
120
|
+
if (
|
|
121
|
+
not isinstance(default, ast.Constant)
|
|
122
|
+
or default.value is not None
|
|
123
|
+
or parameter.annotation is None
|
|
124
|
+
or not _is_nullable_union(parameter.annotation)
|
|
125
|
+
):
|
|
126
|
+
continue
|
|
127
|
+
return parameter
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _immediately_rejected_parameter(method: ast.FunctionDef) -> str | None:
|
|
132
|
+
body = method.body
|
|
133
|
+
if (
|
|
134
|
+
body
|
|
135
|
+
and isinstance(body[0], ast.Expr)
|
|
136
|
+
and isinstance(body[0].value, ast.Constant)
|
|
137
|
+
and isinstance(body[0].value.value, str)
|
|
138
|
+
):
|
|
139
|
+
body = body[1:]
|
|
140
|
+
if not body:
|
|
141
|
+
return None
|
|
142
|
+
first = body[0]
|
|
143
|
+
if (
|
|
144
|
+
isinstance(first, ast.If)
|
|
145
|
+
and isinstance(first.body[-1], ast.Raise)
|
|
146
|
+
and all(isinstance(statement, (ast.Assign, ast.AnnAssign)) for statement in first.body[:-1])
|
|
147
|
+
):
|
|
148
|
+
condition, operator = first.test, ast.Is
|
|
149
|
+
elif isinstance(first, ast.Assert):
|
|
150
|
+
condition, operator = first.test, ast.IsNot
|
|
151
|
+
else:
|
|
152
|
+
return None
|
|
153
|
+
if not isinstance(condition, ast.Compare) or len(condition.ops) != 1 or not isinstance(condition.ops[0], operator):
|
|
154
|
+
return None
|
|
155
|
+
left, right = condition.left, condition.comparators[0]
|
|
156
|
+
for name, absent in ((left, right), (right, left)):
|
|
157
|
+
if isinstance(name, ast.Name) and isinstance(absent, ast.Constant) and absent.value is None:
|
|
158
|
+
return name.id
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _is_nullable_union(annotation: ast.expr) -> bool:
|
|
163
|
+
if not isinstance(annotation, ast.BinOp) or not isinstance(annotation.op, ast.BitOr):
|
|
164
|
+
return False
|
|
165
|
+
return _is_none_union_member(annotation.left) or _is_none_union_member(annotation.right)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _is_none_union_member(annotation: ast.expr) -> bool:
|
|
169
|
+
if isinstance(annotation, ast.BinOp) and isinstance(annotation.op, ast.BitOr):
|
|
170
|
+
return _is_none_union_member(annotation.left) or _is_none_union_member(annotation.right)
|
|
171
|
+
return isinstance(annotation, ast.Constant) and annotation.value is None
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
+
from dataclasses import replace
|
|
4
5
|
from pathlib import PurePosixPath
|
|
6
|
+
from types import MappingProxyType
|
|
5
7
|
from typing import TYPE_CHECKING, ClassVar, final
|
|
6
8
|
|
|
7
9
|
from sarj_python_lint.rule_base import (
|
|
@@ -18,6 +20,7 @@ from sarj_python_lint.rule_base import (
|
|
|
18
20
|
)
|
|
19
21
|
from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics, scope_bound_names
|
|
20
22
|
from sarj_python_lint.rules._ast_index import walk
|
|
23
|
+
from sarj_python_lint.rules.no_hidden_constructor_fallback import LocalBindingCollector, scope_bindings
|
|
21
24
|
|
|
22
25
|
|
|
23
26
|
if TYPE_CHECKING:
|
|
@@ -62,7 +65,7 @@ class NoBroadKeywordCapture(Rule):
|
|
|
62
65
|
"Reports missing, Any- or object-valued keyword captures whether discarded, read or forwarded.",
|
|
63
66
|
"Precise or unresolved decorated signature-declaration families and functions with unresolved decorator effects are excluded; known classmethod/staticmethod/abstractmethod/final/override declarations remain eligible.",
|
|
64
67
|
"Homogeneous keyword types constrain values but allow arbitrary names; ParamSpec preserves only its target's existing contract.",
|
|
65
|
-
"Generated/vendor source and stubs are excluded. Unresolved imports, wildcard imports and shadowed annotation provenance are conservative.",
|
|
68
|
+
"Generated/vendor source and stubs are excluded. Unresolved imports, wildcard imports, conflicting imports, actual global or definition-time writes and shadowed annotation provenance are conservative. Identical imports and read-only global declarations remain eligible.",
|
|
66
69
|
"Cross-file aliases, external stubs, generated signatures and runtime reflection are not resolved; transparent annotation metadata and ambiguous decorated declaration families can cause conservative false negatives.",
|
|
67
70
|
"No autofix can infer the intended contract. Dynamic payload/formatting APIs can intentionally need an exact-code exception.",
|
|
68
71
|
),
|
|
@@ -165,7 +168,8 @@ class NoBroadKeywordCapture(Rule):
|
|
|
165
168
|
tree = context.tree
|
|
166
169
|
if tree is None:
|
|
167
170
|
return []
|
|
168
|
-
semantics =
|
|
171
|
+
semantics = _stable_annotation_semantics(context, tree)
|
|
172
|
+
mutated_members = _mutated_annotation_members(context, semantics)
|
|
169
173
|
overloads: set[ast.FunctionDef | ast.AsyncFunctionDef] | None = None
|
|
170
174
|
findings: list[Diagnostic] = []
|
|
171
175
|
for function in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef):
|
|
@@ -174,12 +178,12 @@ class NoBroadKeywordCapture(Rule):
|
|
|
174
178
|
continue
|
|
175
179
|
if not _is_broad(argument.annotation, semantics):
|
|
176
180
|
continue
|
|
177
|
-
if _uncertain_annotation(argument.annotation, function, context, semantics):
|
|
181
|
+
if _uncertain_annotation(argument.annotation, function, context, semantics, mutated_members):
|
|
178
182
|
continue
|
|
179
|
-
if _unknown_decorator(function, context, semantics):
|
|
183
|
+
if _unknown_decorator(function, context, semantics, mutated_members):
|
|
180
184
|
continue
|
|
181
185
|
if overloads is None:
|
|
182
|
-
overloads = _overload_implementations(context, semantics)
|
|
186
|
+
overloads = _overload_implementations(context, semantics, mutated_members)
|
|
183
187
|
if function in overloads:
|
|
184
188
|
continue
|
|
185
189
|
if is_suppressed(context.source_lines, argument.lineno, self.code) or is_suppressed(
|
|
@@ -202,6 +206,90 @@ class NoBroadKeywordCapture(Rule):
|
|
|
202
206
|
return sorted(findings, key=lambda finding: (finding.line, finding.col))
|
|
203
207
|
|
|
204
208
|
|
|
209
|
+
def _stable_annotation_semantics(context: PythonFileContext, tree: ast.Module) -> AnnotationSemantics:
|
|
210
|
+
semantics = AnnotationSemantics.from_tree(tree)
|
|
211
|
+
unstable = _conflicting_annotation_imports(context, semantics) | _annotation_binding_writes(context)
|
|
212
|
+
if not unstable:
|
|
213
|
+
return semantics
|
|
214
|
+
imports = replace(
|
|
215
|
+
semantics.imports,
|
|
216
|
+
bindings=MappingProxyType(
|
|
217
|
+
{name: target for name, target in semantics.imports.bindings.items() if name not in unstable}
|
|
218
|
+
),
|
|
219
|
+
shadowed_names=semantics.imports.shadowed_names | unstable,
|
|
220
|
+
)
|
|
221
|
+
return replace(
|
|
222
|
+
semantics,
|
|
223
|
+
imports=imports,
|
|
224
|
+
aliases=MappingProxyType({name: value for name, value in semantics.aliases.items() if name not in unstable}),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _conflicting_annotation_imports(context: PythonFileContext, semantics: AnnotationSemantics) -> set[str]:
|
|
229
|
+
conflicts: set[str] = set()
|
|
230
|
+
for statement in context.nodes(ast.Import, ast.ImportFrom):
|
|
231
|
+
if not isinstance(_lexical_scope(statement, context), ast.Module):
|
|
232
|
+
continue
|
|
233
|
+
for alias in statement.names:
|
|
234
|
+
name = alias.asname or alias.name.partition(".")[0]
|
|
235
|
+
binding = semantics.imports.bindings.get(name)
|
|
236
|
+
if binding is not None and _import_reference(statement, alias) != (binding.module, binding.symbol):
|
|
237
|
+
conflicts.add(name)
|
|
238
|
+
return conflicts
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _import_reference(statement: ast.Import | ast.ImportFrom, alias: ast.alias) -> tuple[str, str | None] | None:
|
|
242
|
+
if isinstance(statement, ast.ImportFrom):
|
|
243
|
+
return (statement.module, alias.name) if not statement.level and statement.module is not None else None
|
|
244
|
+
return (alias.name if alias.asname else alias.name.partition(".")[0], None)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _annotation_binding_writes(context: PythonFileContext) -> set[str]:
|
|
248
|
+
writes: set[str] = set()
|
|
249
|
+
for owner in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda):
|
|
250
|
+
if isinstance(_lexical_scope(owner, context), ast.Module):
|
|
251
|
+
header = LocalBindingCollector()
|
|
252
|
+
header.visit(owner)
|
|
253
|
+
if not isinstance(owner, ast.Lambda):
|
|
254
|
+
header.names.discard(owner.name)
|
|
255
|
+
writes.update(header.names)
|
|
256
|
+
if not isinstance(owner, ast.Lambda):
|
|
257
|
+
body = LocalBindingCollector()
|
|
258
|
+
for statement in owner.body:
|
|
259
|
+
body.visit(statement)
|
|
260
|
+
writes.update(body.names & body.globals)
|
|
261
|
+
return writes
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _mutated_annotation_members(context: PythonFileContext, semantics: AnnotationSemantics) -> frozenset[str]:
|
|
265
|
+
members: set[str] = set()
|
|
266
|
+
for node in context.nodes(ast.Attribute):
|
|
267
|
+
if not isinstance(node.ctx, (ast.Store, ast.Del)) or _shadowed_annotation(node, node, context, semantics):
|
|
268
|
+
continue
|
|
269
|
+
qualified = semantics.imports.resolved_qualified_name(node)
|
|
270
|
+
if qualified is not None:
|
|
271
|
+
members.add(qualified)
|
|
272
|
+
return frozenset(members)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _uses_mutated_member(annotation: ast.expr, semantics: AnnotationSemantics, mutated_members: frozenset[str]) -> bool:
|
|
276
|
+
if not mutated_members:
|
|
277
|
+
return False
|
|
278
|
+
pending = [annotation]
|
|
279
|
+
seen: set[str] = set()
|
|
280
|
+
while pending:
|
|
281
|
+
parsed = semantics.parse(pending.pop())
|
|
282
|
+
if parsed is None:
|
|
283
|
+
continue
|
|
284
|
+
for node in walk(parsed):
|
|
285
|
+
if isinstance(node, ast.Attribute) and semantics.imports.resolved_qualified_name(node) in mutated_members:
|
|
286
|
+
return True
|
|
287
|
+
if isinstance(node, ast.Name) and node.id in semantics.aliases and node.id not in seen:
|
|
288
|
+
seen.add(node.id)
|
|
289
|
+
pending.append(semantics.aliases[node.id])
|
|
290
|
+
return False
|
|
291
|
+
|
|
292
|
+
|
|
205
293
|
def _is_broad(annotation: ast.expr | None, semantics: AnnotationSemantics) -> bool:
|
|
206
294
|
if annotation is None:
|
|
207
295
|
return True
|
|
@@ -215,29 +303,35 @@ def _is_broad(annotation: ast.expr | None, semantics: AnnotationSemantics) -> bo
|
|
|
215
303
|
|
|
216
304
|
def _shadowed_annotation(
|
|
217
305
|
annotation: ast.expr | None,
|
|
218
|
-
function: ast.
|
|
306
|
+
function: ast.AST,
|
|
219
307
|
context: PythonFileContext,
|
|
220
308
|
semantics: AnnotationSemantics,
|
|
221
309
|
) -> bool:
|
|
222
310
|
names = _uses_names(annotation, semantics)
|
|
223
|
-
if names & _type_parameter_names(
|
|
311
|
+
if isinstance(function, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) and names & _type_parameter_names(
|
|
312
|
+
function
|
|
313
|
+
):
|
|
224
314
|
return True
|
|
225
315
|
parent = context.parents.get(function)
|
|
226
316
|
while parent is not None:
|
|
227
|
-
if isinstance(
|
|
228
|
-
parent
|
|
229
|
-
):
|
|
317
|
+
if isinstance(
|
|
318
|
+
parent, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)
|
|
319
|
+
) and names & _scope_bindings(parent):
|
|
230
320
|
return True
|
|
231
321
|
parent = context.parents.get(parent)
|
|
232
322
|
return False
|
|
233
323
|
|
|
234
324
|
|
|
235
|
-
def _scope_bindings(owner: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef) -> set[str]:
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
325
|
+
def _scope_bindings(owner: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Lambda) -> set[str]:
|
|
326
|
+
if isinstance(owner, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)):
|
|
327
|
+
bound = set(scope_bindings(owner))
|
|
328
|
+
else:
|
|
329
|
+
collector = LocalBindingCollector()
|
|
330
|
+
for statement in owner.body:
|
|
331
|
+
collector.visit(statement)
|
|
332
|
+
bound = collector.names - collector.globals
|
|
333
|
+
if not isinstance(owner, ast.Lambda):
|
|
334
|
+
bound.update(_type_parameter_names(owner))
|
|
241
335
|
return bound
|
|
242
336
|
|
|
243
337
|
|
|
@@ -250,10 +344,11 @@ def _uncertain_annotation(
|
|
|
250
344
|
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
251
345
|
context: PythonFileContext,
|
|
252
346
|
semantics: AnnotationSemantics,
|
|
347
|
+
mutated_members: frozenset[str],
|
|
253
348
|
) -> bool:
|
|
254
349
|
if annotation is None:
|
|
255
350
|
return False
|
|
256
|
-
if _has_wildcard_import(context):
|
|
351
|
+
if _has_wildcard_import(context) or _uses_mutated_member(annotation, semantics, mutated_members):
|
|
257
352
|
return True
|
|
258
353
|
if context.tree is not None:
|
|
259
354
|
unknown = scope_bound_names(context.tree.body) - set(semantics.imports.bindings) - set(semantics.aliases)
|
|
@@ -280,8 +375,9 @@ def _known_decorator(
|
|
|
280
375
|
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
281
376
|
context: PythonFileContext,
|
|
282
377
|
semantics: AnnotationSemantics,
|
|
378
|
+
mutated_members: frozenset[str],
|
|
283
379
|
) -> str | None:
|
|
284
|
-
if _has_wildcard_import(context):
|
|
380
|
+
if _has_wildcard_import(context) or _uses_mutated_member(decorator, semantics, mutated_members):
|
|
285
381
|
return None
|
|
286
382
|
if _shadowed_annotation(decorator, function, context, semantics):
|
|
287
383
|
return None
|
|
@@ -297,9 +393,10 @@ def _unknown_decorator(
|
|
|
297
393
|
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
298
394
|
context: PythonFileContext,
|
|
299
395
|
semantics: AnnotationSemantics,
|
|
396
|
+
mutated_members: frozenset[str],
|
|
300
397
|
) -> bool:
|
|
301
398
|
return any(
|
|
302
|
-
_known_decorator(decorator, function, context, semantics) not in _KNOWN_DECORATORS
|
|
399
|
+
_known_decorator(decorator, function, context, semantics, mutated_members) not in _KNOWN_DECORATORS
|
|
303
400
|
for decorator in function.decorator_list
|
|
304
401
|
)
|
|
305
402
|
|
|
@@ -307,6 +404,7 @@ def _unknown_decorator(
|
|
|
307
404
|
def _overload_implementations(
|
|
308
405
|
context: PythonFileContext,
|
|
309
406
|
semantics: AnnotationSemantics,
|
|
407
|
+
mutated_members: frozenset[str],
|
|
310
408
|
) -> set[ast.FunctionDef | ast.AsyncFunctionDef]:
|
|
311
409
|
families: dict[tuple[ast.AST | None, str], list[ast.FunctionDef | ast.AsyncFunctionDef]] = {}
|
|
312
410
|
implementations: set[ast.FunctionDef | ast.AsyncFunctionDef] = set()
|
|
@@ -315,7 +413,7 @@ def _overload_implementations(
|
|
|
315
413
|
scope = _lexical_scope(node, context)
|
|
316
414
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
317
415
|
key = scope, node.name
|
|
318
|
-
if _signature_declaration(node, context, semantics):
|
|
416
|
+
if _signature_declaration(node, context, semantics, mutated_members):
|
|
319
417
|
families.setdefault(key, []).append(node)
|
|
320
418
|
else:
|
|
321
419
|
family = families.pop(key, [])
|
|
@@ -356,6 +454,10 @@ def _signature_declaration(
|
|
|
356
454
|
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
357
455
|
context: PythonFileContext,
|
|
358
456
|
semantics: AnnotationSemantics,
|
|
457
|
+
mutated_members: frozenset[str],
|
|
359
458
|
) -> bool:
|
|
360
|
-
decorators = {
|
|
459
|
+
decorators = {
|
|
460
|
+
_known_decorator(decorator, function, context, semantics, mutated_members)
|
|
461
|
+
for decorator in function.decorator_list
|
|
462
|
+
}
|
|
361
463
|
return bool(decorators & _OVERLOAD_DECORATORS) or not decorators <= _KNOWN_DECORATORS
|
|
@@ -41,7 +41,7 @@ class NoDatabaseProgrammability(Rule):
|
|
|
41
41
|
default_level=Severity.ERROR,
|
|
42
42
|
summary="Keep stored SQL functions and triggers out of embedded SQL.",
|
|
43
43
|
rationale="SQL hidden in Python can bypass migration-only architecture rules. Procedural fault fixtures require extra database objects and cleanup when a declarative constraint suffices.",
|
|
44
|
-
remediation="
|
|
44
|
+
remediation="Prefer declarative constraints or explicit transactional application behavior where equivalent, including in test fixtures. Preserve transaction atomicity and database-enforced integrity; use an exact SARJ470 suppression for an approved database-owned invariant or compatibility exception.",
|
|
45
45
|
category=RuleCategory.ARCHITECTURE,
|
|
46
46
|
autofix=AutofixPolicy.NONE,
|
|
47
47
|
limitations=(
|
|
@@ -92,7 +92,7 @@ class NoDatabaseProgrammability(Rule):
|
|
|
92
92
|
col=node.col_offset + 1,
|
|
93
93
|
code=self.code,
|
|
94
94
|
severity=Severity.ERROR,
|
|
95
|
-
message="
|
|
95
|
+
message="Project architecture prefers declarative constraints or explicit transactional application behavior, including in test fixtures. Preserve atomicity and database-enforced integrity; use an exact SARJ470 suppression for an approved database-owned invariant or compatibility exception.",
|
|
96
96
|
)
|
|
97
97
|
)
|
|
98
98
|
return sorted(diagnostics, key=lambda item: (item.line, item.col))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
+
from itertools import chain
|
|
4
5
|
from pathlib import PurePosixPath
|
|
5
6
|
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
6
7
|
|
|
@@ -45,13 +46,14 @@ class NoInjectedModuleLoader(Rule):
|
|
|
45
46
|
code = "SARJ471"
|
|
46
47
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
47
48
|
default_level=Severity.WARNING,
|
|
48
|
-
summary="
|
|
49
|
-
rationale="
|
|
50
|
-
remediation="
|
|
49
|
+
summary="Avoid a generic module-loader parameter used only to import one fixed module.",
|
|
50
|
+
rationale="When every direct use imports the same literal module, a generic importer can be unnecessary customization rather than a domain dependency. Dynamic plugin/import APIs legitimately need import hooks.",
|
|
51
|
+
remediation="Consider owning the fixed import inside the implementation when import-hook substitution is unnecessary. Preserve intentional import hooks and plugin APIs; do not add wrapper factories or Protocols just to silence this advisory.",
|
|
51
52
|
category=RuleCategory.ARCHITECTURE,
|
|
52
53
|
autofix=AutofixPolicy.NONE,
|
|
53
54
|
limitations=(
|
|
54
55
|
"Flags defaults resolving to importlib.import_module, importlib.__import__, or builtins.__import__, including stable module-level import and single-assignment aliases and unshadowed __import__.",
|
|
56
|
+
"Every body reference to the parameter must be a direct call with one identical literal module name. Dynamic names, multiple module names, forwarding, returned/captured hooks, rebinding, unpacking and unused defaults are excluded.",
|
|
55
57
|
"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.",
|
|
56
58
|
"Conservatively skips shadowed or rebound names, wildcard-import files, reassigned loader attributes, conflicting conditional or relative imports, and exception or pattern captures of importer roots, symbols or assignment aliases. Identical repeated importer imports remain valid. A relevant-name capture in an unrelated local scope can cause a false negative because capture checks are file-wide.",
|
|
57
59
|
"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.",
|
|
@@ -110,7 +112,8 @@ class NoInjectedModuleLoader(Rule):
|
|
|
110
112
|
and (aliases[default.id].lineno, aliases[default.id].col_offset)
|
|
111
113
|
< (function.lineno, function.col_offset)
|
|
112
114
|
)
|
|
113
|
-
|
|
115
|
+
module_name = _fixed_module_name(context, function, argument.arg)
|
|
116
|
+
if not (direct or aliased) or _shadowed(default, function, context) or module_name is None:
|
|
114
117
|
continue
|
|
115
118
|
findings.append(
|
|
116
119
|
Diagnostic(
|
|
@@ -118,13 +121,43 @@ class NoInjectedModuleLoader(Rule):
|
|
|
118
121
|
line=argument.lineno,
|
|
119
122
|
col=argument.col_offset + 1,
|
|
120
123
|
code=self.code,
|
|
121
|
-
message=
|
|
124
|
+
message=(
|
|
125
|
+
f"Parameter `{argument.arg}` exposes a generic module loader but only imports "
|
|
126
|
+
f"`{module_name}`; consider an internal import unless this import hook is intentional."
|
|
127
|
+
),
|
|
122
128
|
severity=Severity.WARNING,
|
|
123
129
|
)
|
|
124
130
|
)
|
|
125
131
|
return sorted(findings, key=lambda finding: (finding.line, finding.col))
|
|
126
132
|
|
|
127
133
|
|
|
134
|
+
def _fixed_module_name(
|
|
135
|
+
context: PythonFileContext, function: ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda, name: str
|
|
136
|
+
) -> str | None:
|
|
137
|
+
body = [function.body] if isinstance(function, ast.Lambda) else function.body
|
|
138
|
+
module_names: set[str] = set()
|
|
139
|
+
for reference in chain.from_iterable(ast.walk(statement) for statement in body):
|
|
140
|
+
if not isinstance(reference, ast.Name) or reference.id != name:
|
|
141
|
+
continue
|
|
142
|
+
call = context.parents.get(reference)
|
|
143
|
+
if not isinstance(reference.ctx, ast.Load) or not isinstance(call, ast.Call) or call.func is not reference:
|
|
144
|
+
return None
|
|
145
|
+
if (
|
|
146
|
+
call.keywords
|
|
147
|
+
or len(call.args) != 1
|
|
148
|
+
or not isinstance(call.args[0], ast.Constant)
|
|
149
|
+
or not isinstance(call.args[0].value, str)
|
|
150
|
+
):
|
|
151
|
+
return None
|
|
152
|
+
parent = context.parents.get(call)
|
|
153
|
+
while parent is not None and parent is not function:
|
|
154
|
+
if isinstance(parent, _LEXICAL_SCOPES):
|
|
155
|
+
return None
|
|
156
|
+
parent = context.parents.get(parent)
|
|
157
|
+
module_names.add(call.args[0].value)
|
|
158
|
+
return next(iter(module_names)) if len(module_names) == 1 else None
|
|
159
|
+
|
|
160
|
+
|
|
128
161
|
def _ambiguous_loader_bindings(context: PythonFileContext, aliases: Mapping[str, ast.stmt]) -> bool:
|
|
129
162
|
if any(alias.name == "*" for node in context.nodes(ast.ImportFrom) for alias in node.names):
|
|
130
163
|
return True
|
|
@@ -66,7 +66,7 @@ class NoNullableDependencyFallback(Rule):
|
|
|
66
66
|
autofix=AutofixPolicy.NONE,
|
|
67
67
|
limitations=(
|
|
68
68
|
"Warns for None-defaulted function parameters with a fallback used as a callable or a proven application-settings fallback; genuine absent state is allowed.",
|
|
69
|
-
"
|
|
69
|
+
"Constructor settings fallbacks remain owned by SARJ095; constructor nullability alone is not a defect. Decorated functions, tests, generated code, nested scope captures, non-callable object fallbacks, and interprocedural forwarding are excluded.",
|
|
70
70
|
"Callable use through a local assignment, annotation or None guard requires at most one body binding for that name and a call after the binding statement completes. Rebinding, deletion, imports, definitions, pattern or exception captures, global/nonlocal declarations and nested-scope bindings conservatively exclude that inference; direct inline calls remain independent. Settings provenance also excludes enclosing bindings and names written by a function declaring them global, while read-only global declarations remain valid.",
|
|
71
71
|
"No autofix: removing explicit None acceptance changes the callable contract, and moving a fallback can change initialization timing or error handling.",
|
|
72
72
|
),
|
|
@@ -39,7 +39,8 @@ class NoUnusedUnderscoredKeywordParameter(Rule):
|
|
|
39
39
|
),
|
|
40
40
|
remediation=(
|
|
41
41
|
"Remove the unused keyword-only parameter and update callers together. If an external contract requires "
|
|
42
|
-
"the input, preserve its required spelling and use a narrow, reasoned unused-argument suppression."
|
|
42
|
+
"the input, preserve its required spelling and use a narrow, reasoned unused-argument suppression. "
|
|
43
|
+
"Do not erase a precise callback signature with **kwargs or add dummy reads to silence this warning."
|
|
43
44
|
),
|
|
44
45
|
category=RuleCategory.MAINTAINABILITY,
|
|
45
46
|
autofix=AutofixPolicy.NONE,
|
|
@@ -48,7 +49,7 @@ class NoUnusedUnderscoredKeywordParameter(Rule):
|
|
|
48
49
|
"Decorated functions other than unshadowed builtin staticmethod/classmethod, inherited methods, and intentional stubs are excluded.",
|
|
49
50
|
"Any reference within the body counts as use, including nested scopes; shadowing and reassignment can produce false negatives.",
|
|
50
51
|
"Functions accessing locals, vars, eval, or exec through recognized builtin bindings are excluded.",
|
|
51
|
-
"
|
|
52
|
+
"Callables referenced as values are excluded because their external keyword contract is unknown. Lambda parameters, unknown dynamic reflection, and cross-module callback contracts are not resolved.",
|
|
52
53
|
"Generated and vendored files are excluded; test source is checked.",
|
|
53
54
|
),
|
|
54
55
|
examples=(
|
|
@@ -103,7 +104,12 @@ def _unused_parameters(
|
|
|
103
104
|
context: PythonFileContext, function: ast.FunctionDef | ast.AsyncFunctionDef
|
|
104
105
|
) -> tuple[ast.arg, ...]:
|
|
105
106
|
candidates = [argument for argument in function.args.kwonlyargs if argument.arg.startswith("_")]
|
|
106
|
-
if
|
|
107
|
+
if (
|
|
108
|
+
not candidates
|
|
109
|
+
or _has_contract(context, function)
|
|
110
|
+
or _is_stub(function)
|
|
111
|
+
or _escapes_as_callback(context, function)
|
|
112
|
+
):
|
|
107
113
|
return ()
|
|
108
114
|
body = list(chain.from_iterable(ast.walk(statement) for statement in function.body))
|
|
109
115
|
if _reads_dynamic_locals(context, function, body):
|
|
@@ -174,3 +180,15 @@ def _reads_dynamic_locals(
|
|
|
174
180
|
)
|
|
175
181
|
for node in body
|
|
176
182
|
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _escapes_as_callback(context: PythonFileContext, function: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
186
|
+
for reference in context.nodes(ast.Name, ast.Attribute):
|
|
187
|
+
if (isinstance(reference, ast.Name) and reference.id != function.name) or (
|
|
188
|
+
isinstance(reference, ast.Attribute) and reference.attr != function.name
|
|
189
|
+
):
|
|
190
|
+
continue
|
|
191
|
+
parent = context.parents.get(reference)
|
|
192
|
+
if isinstance(reference.ctx, ast.Load) and not (isinstance(parent, ast.Call) and parent.func is reference):
|
|
193
|
+
return True
|
|
194
|
+
return False
|
|
@@ -46,6 +46,7 @@ class NoUnusedValueMarker(Rule):
|
|
|
46
46
|
"Plain and annotated assignments whose standalone target is `_` are reported, including chained assignments.",
|
|
47
47
|
"Formal-parameter references are reported too, including positional, keyword-only, and variadic parameters.",
|
|
48
48
|
"Unpacking placeholders, loop targets, wildcard imports, and underscore-prefixed names are excluded.",
|
|
49
|
+
"A binding read in the same enclosing scope is not a discard. Reads in nested scopes conservatively preserve it, even when shadowing cannot be resolved.",
|
|
49
50
|
"Generated and vendored files are excluded.",
|
|
50
51
|
),
|
|
51
52
|
examples=(
|
|
@@ -148,7 +149,9 @@ class NoUnusedValueMarker(Rule):
|
|
|
148
149
|
markers = [
|
|
149
150
|
node
|
|
150
151
|
for node in context.nodes(ast.AST)
|
|
151
|
-
if isinstance(node, (ast.Assign, ast.AnnAssign))
|
|
152
|
+
if isinstance(node, (ast.Assign, ast.AnnAssign))
|
|
153
|
+
and _is_unused_value_marker(node)
|
|
154
|
+
and not _scope_reads_marker(context, node)
|
|
152
155
|
]
|
|
153
156
|
if not markers:
|
|
154
157
|
return []
|
|
@@ -173,3 +176,14 @@ def _is_unused_value_marker(node: ast.Assign | ast.AnnAssign) -> bool:
|
|
|
173
176
|
if isinstance(node, ast.AnnAssign):
|
|
174
177
|
return node.value is not None and isinstance(node.target, ast.Name) and node.target.id == "_"
|
|
175
178
|
return any(isinstance(target, ast.Name) and target.id == "_" for target in node.targets)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _scope_reads_marker(context: PythonFileContext, marker: ast.Assign | ast.AnnAssign) -> bool:
|
|
182
|
+
scope: ast.AST = marker
|
|
183
|
+
while (parent := context.parents.get(scope)) is not None:
|
|
184
|
+
scope = parent
|
|
185
|
+
if isinstance(scope, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Module):
|
|
186
|
+
break
|
|
187
|
+
return any(
|
|
188
|
+
isinstance(node, ast.Name) and node.id == "_" and isinstance(node.ctx, ast.Load) for node in ast.walk(scope)
|
|
189
|
+
)
|