sarj-python-lint 1.15.2__tar.gz → 1.16.1__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.15.2 → sarj_python_lint-1.16.1}/PKG-INFO +1 -1
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/pyproject.toml +1 -1
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_registry.py +2 -0
- sarj_python_lint-1.16.1/src/sarj_python_lint/rules/discourage_nullable_constructor_parameters.py +171 -0
- sarj_python_lint-1.16.1/src/sarj_python_lint/rules/no_broad_keyword_capture.py +361 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_database_programmability.py +2 -2
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_injected_module_loader.py +38 -5
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +1 -1
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +21 -3
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +15 -1
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +22 -14
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +78 -10
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +35 -14
- sarj_python_lint-1.16.1/src/sarj_python_lint/rules/require_explicit_service_contract.py +579 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/require_precise_factory_signature.py +12 -2
- sarj_python_lint-1.16.1/src/sarj_python_lint/rules/require_public_dependency_contract.py +248 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/unused_test_factory_option.py +25 -101
- sarj_python_lint-1.15.2/src/sarj_python_lint/rules/discourage_nullable_constructor_parameters.py +0 -122
- sarj_python_lint-1.15.2/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -338
- sarj_python_lint-1.15.2/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -196
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/.gitignore +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/LICENSE +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/README.md +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.15.2 → sarj_python_lint-1.16.1}/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.
|
|
3
|
+
Version: 1.16.1
|
|
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/
|
|
@@ -33,6 +33,7 @@ from sarj_python_lint.rules.no_analytical_aggregation_in_postgres_store import (
|
|
|
33
33
|
NoAnalyticalAggregationInPostgresStore,
|
|
34
34
|
)
|
|
35
35
|
from sarj_python_lint.rules.no_any_mapping_types import NoAnyMappingTypes
|
|
36
|
+
from sarj_python_lint.rules.no_broad_keyword_capture import NoBroadKeywordCapture
|
|
36
37
|
from sarj_python_lint.rules.no_comment_cruft import NoCommentCruft
|
|
37
38
|
from sarj_python_lint.rules.no_conftest_test_module_import import NoConftestTestModuleImport
|
|
38
39
|
from sarj_python_lint.rules.no_copied_inherited_docstring import NoCopiedInheritedDocstring
|
|
@@ -297,6 +298,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
297
298
|
NoDuplicateDunderAllEntry.id: NoDuplicateDunderAllEntry,
|
|
298
299
|
NoDunderAll.id: NoDunderAll,
|
|
299
300
|
NoDeleteStatement.id: NoDeleteStatement,
|
|
301
|
+
NoBroadKeywordCapture.id: NoBroadKeywordCapture,
|
|
300
302
|
NoCopiedInheritedDocstring.id: NoCopiedInheritedDocstring,
|
|
301
303
|
RedundantClassDocstring.id: RedundantClassDocstring,
|
|
302
304
|
RedundantModuleDocstring.id: RedundantModuleDocstring,
|
sarj_python_lint-1.16.1/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
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar, final
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint.rule_base import (
|
|
8
|
+
AutofixPolicy,
|
|
9
|
+
Diagnostic,
|
|
10
|
+
ExampleFile,
|
|
11
|
+
ExampleOutcome,
|
|
12
|
+
Rule,
|
|
13
|
+
RuleCategory,
|
|
14
|
+
RuleDocumentation,
|
|
15
|
+
RuleExample,
|
|
16
|
+
Severity,
|
|
17
|
+
is_suppressed,
|
|
18
|
+
)
|
|
19
|
+
from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics, scope_bound_names
|
|
20
|
+
from sarj_python_lint.rules._ast_index import walk
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_MIN_OVERLOAD_DECLARATIONS = 2
|
|
28
|
+
_OVERLOAD_DECORATORS = frozenset({"typing.overload", "typing_extensions.overload"})
|
|
29
|
+
_KNOWN_DECORATORS = _OVERLOAD_DECORATORS | {
|
|
30
|
+
"builtins.classmethod",
|
|
31
|
+
"builtins.staticmethod",
|
|
32
|
+
"abc.abstractmethod",
|
|
33
|
+
"typing.final",
|
|
34
|
+
"typing.override",
|
|
35
|
+
"typing_extensions.final",
|
|
36
|
+
"typing_extensions.override",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@final
|
|
41
|
+
class NoBroadKeywordCapture(Rule):
|
|
42
|
+
id = "no-broad-keyword-capture"
|
|
43
|
+
code = "SARJ477"
|
|
44
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
45
|
+
default_level=Severity.WARNING,
|
|
46
|
+
summary="Prefer precise keyword interfaces over missing, Any- or object-valued captures.",
|
|
47
|
+
rationale=(
|
|
48
|
+
"Replacing a fixed typed interface with broad **kwargs can erase required keyword names and per-key "
|
|
49
|
+
"value types, making test doubles accept calls the real implementation rejects. object remains a safe "
|
|
50
|
+
"typed top type, and deliberately open data interfaces can be valid; this warning requests interface "
|
|
51
|
+
"review rather than proving every broad capture is defective."
|
|
52
|
+
),
|
|
53
|
+
remediation=(
|
|
54
|
+
"Keep explicit typed parameters at fixed interfaces. Preserve required keyword names with named unused "
|
|
55
|
+
"local bindings. Use ParamSpec for forwarding, Unpack[TypedDict] for named schemas, or a precise "
|
|
56
|
+
"homogeneous value type. Document genuinely open interfaces with an exact-code reasoned suppression."
|
|
57
|
+
),
|
|
58
|
+
category=RuleCategory.MAINTAINABILITY,
|
|
59
|
+
autofix=AutofixPolicy.NONE,
|
|
60
|
+
limitations=(
|
|
61
|
+
"This is an advisory interface policy, not proof of incorrect behavior or a universal ban on the object type.",
|
|
62
|
+
"Reports missing, Any- or object-valued keyword captures whether discarded, read or forwarded.",
|
|
63
|
+
"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
|
+
"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.",
|
|
66
|
+
"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
|
+
"No autofix can infer the intended contract. Dynamic payload/formatting APIs can intentionally need an exact-code exception.",
|
|
68
|
+
),
|
|
69
|
+
examples=(
|
|
70
|
+
RuleExample(
|
|
71
|
+
example_id="discarded-session-keywords",
|
|
72
|
+
title="An ignored catch-all weakens a fake's contract",
|
|
73
|
+
outcome=ExampleOutcome.MATCH,
|
|
74
|
+
files=(
|
|
75
|
+
ExampleFile.python(
|
|
76
|
+
"tests/fakes/session.py",
|
|
77
|
+
"class Session:\n async def start(self, **_kwargs: object) -> None:\n self.start_count += 1\n",
|
|
78
|
+
),
|
|
79
|
+
),
|
|
80
|
+
focus_path=PurePosixPath("tests/fakes/session.py"),
|
|
81
|
+
expected_count=1,
|
|
82
|
+
public=True,
|
|
83
|
+
),
|
|
84
|
+
RuleExample(
|
|
85
|
+
example_id="typed-unused-keywords",
|
|
86
|
+
title="Preserve named typed keywords with a named unused binding",
|
|
87
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
88
|
+
files=(
|
|
89
|
+
ExampleFile.python(
|
|
90
|
+
"tests/fakes/session.py",
|
|
91
|
+
"class Session:\n async def start(self, *, room: Room, agent: Agent) -> None:\n _unused_inputs = room, agent\n self.start_count += 1\n",
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
focus_path=PurePosixPath("tests/fakes/session.py"),
|
|
95
|
+
expected_count=0,
|
|
96
|
+
public=True,
|
|
97
|
+
),
|
|
98
|
+
RuleExample(
|
|
99
|
+
example_id="broad-forwarding-arguments",
|
|
100
|
+
title="Forwarding broad arguments still loses the callable contract",
|
|
101
|
+
outcome=ExampleOutcome.MATCH,
|
|
102
|
+
files=(
|
|
103
|
+
ExampleFile.python(
|
|
104
|
+
"app/adapter.py",
|
|
105
|
+
"def wrapper(*args: object, **kwargs: object):\n return target(*args, **kwargs)\n",
|
|
106
|
+
),
|
|
107
|
+
),
|
|
108
|
+
focus_path=PurePosixPath("app/adapter.py"),
|
|
109
|
+
expected_count=1,
|
|
110
|
+
public=True,
|
|
111
|
+
scenario="forwarding",
|
|
112
|
+
),
|
|
113
|
+
RuleExample(
|
|
114
|
+
example_id="typed-forwarding-arguments",
|
|
115
|
+
title="ParamSpec preserves the forwarded callable contract",
|
|
116
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
117
|
+
files=(
|
|
118
|
+
ExampleFile.python(
|
|
119
|
+
"app/adapter.py",
|
|
120
|
+
"from collections.abc import Callable\ndef wrapper[**P, R](target: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> R:\n return target(*args, **kwargs)\n",
|
|
121
|
+
),
|
|
122
|
+
),
|
|
123
|
+
focus_path=PurePosixPath("app/adapter.py"),
|
|
124
|
+
expected_count=0,
|
|
125
|
+
public=True,
|
|
126
|
+
scenario="forwarding",
|
|
127
|
+
),
|
|
128
|
+
RuleExample(
|
|
129
|
+
example_id="open-template-fields",
|
|
130
|
+
title="A deliberately open formatting interface still requests policy review",
|
|
131
|
+
outcome=ExampleOutcome.MATCH,
|
|
132
|
+
files=(
|
|
133
|
+
ExampleFile.python(
|
|
134
|
+
"app/templates.py",
|
|
135
|
+
"def render(template: str, **values: object) -> str:\n return template.format(**values)\n",
|
|
136
|
+
),
|
|
137
|
+
),
|
|
138
|
+
focus_path=PurePosixPath("app/templates.py"),
|
|
139
|
+
expected_count=1,
|
|
140
|
+
public=True,
|
|
141
|
+
scenario="dynamic-boundary",
|
|
142
|
+
),
|
|
143
|
+
RuleExample(
|
|
144
|
+
example_id="documented-open-template-fields",
|
|
145
|
+
title="Document why template fields require an open object-valued interface",
|
|
146
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
147
|
+
files=(
|
|
148
|
+
ExampleFile.python(
|
|
149
|
+
"app/templates.py",
|
|
150
|
+
"def render(template: str, **values: object) -> str: # sarj-noqa: SARJ477 — template supplies dynamic field names and object formatting\n return template.format(**values)\n",
|
|
151
|
+
),
|
|
152
|
+
),
|
|
153
|
+
focus_path=PurePosixPath("app/templates.py"),
|
|
154
|
+
expected_count=0,
|
|
155
|
+
public=True,
|
|
156
|
+
scenario="dynamic-boundary",
|
|
157
|
+
),
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
description = documentation.summary
|
|
161
|
+
|
|
162
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
163
|
+
if "*" not in context.source or context.generated or context.path.suffix == ".pyi":
|
|
164
|
+
return []
|
|
165
|
+
tree = context.tree
|
|
166
|
+
if tree is None:
|
|
167
|
+
return []
|
|
168
|
+
semantics = AnnotationSemantics.from_tree(tree)
|
|
169
|
+
overloads: set[ast.FunctionDef | ast.AsyncFunctionDef] | None = None
|
|
170
|
+
findings: list[Diagnostic] = []
|
|
171
|
+
for function in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef):
|
|
172
|
+
argument = function.args.kwarg
|
|
173
|
+
if argument is None:
|
|
174
|
+
continue
|
|
175
|
+
if not _is_broad(argument.annotation, semantics):
|
|
176
|
+
continue
|
|
177
|
+
if _uncertain_annotation(argument.annotation, function, context, semantics):
|
|
178
|
+
continue
|
|
179
|
+
if _unknown_decorator(function, context, semantics):
|
|
180
|
+
continue
|
|
181
|
+
if overloads is None:
|
|
182
|
+
overloads = _overload_implementations(context, semantics)
|
|
183
|
+
if function in overloads:
|
|
184
|
+
continue
|
|
185
|
+
if is_suppressed(context.source_lines, argument.lineno, self.code) or is_suppressed(
|
|
186
|
+
context.source_lines, function.lineno, self.code
|
|
187
|
+
):
|
|
188
|
+
continue
|
|
189
|
+
findings.append(
|
|
190
|
+
Diagnostic(
|
|
191
|
+
path=context.path,
|
|
192
|
+
line=argument.lineno,
|
|
193
|
+
col=argument.col_offset + 1,
|
|
194
|
+
code=self.code,
|
|
195
|
+
severity=Severity.WARNING,
|
|
196
|
+
message=(
|
|
197
|
+
f"keyword capture `{argument.arg}` has no declared names or narrow value type; "
|
|
198
|
+
"prefer an explicit typed interface or document a deliberately open contract."
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
return sorted(findings, key=lambda finding: (finding.line, finding.col))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _is_broad(annotation: ast.expr | None, semantics: AnnotationSemantics) -> bool:
|
|
206
|
+
if annotation is None:
|
|
207
|
+
return True
|
|
208
|
+
return any(
|
|
209
|
+
(isinstance(member, ast.Name) and member.id == "object" and semantics.imports.builtin_is_unshadowed("object"))
|
|
210
|
+
or semantics.imports.resolved_qualified_name(member)
|
|
211
|
+
in {"typing.Any", "typing_extensions.Any", "builtins.object"}
|
|
212
|
+
for member in semantics.transparent_members(annotation)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _shadowed_annotation(
|
|
217
|
+
annotation: ast.expr | None,
|
|
218
|
+
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
219
|
+
context: PythonFileContext,
|
|
220
|
+
semantics: AnnotationSemantics,
|
|
221
|
+
) -> bool:
|
|
222
|
+
names = _uses_names(annotation, semantics)
|
|
223
|
+
if names & _type_parameter_names(function):
|
|
224
|
+
return True
|
|
225
|
+
parent = context.parents.get(function)
|
|
226
|
+
while parent is not None:
|
|
227
|
+
if isinstance(parent, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) and names & _scope_bindings(
|
|
228
|
+
parent
|
|
229
|
+
):
|
|
230
|
+
return True
|
|
231
|
+
parent = context.parents.get(parent)
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _scope_bindings(owner: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef) -> set[str]:
|
|
236
|
+
bound = scope_bound_names(owner.body) | _type_parameter_names(owner)
|
|
237
|
+
if isinstance(owner, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
238
|
+
args = owner.args
|
|
239
|
+
bound.update(arg.arg for arg in [*args.posonlyargs, *args.args, *args.kwonlyargs])
|
|
240
|
+
bound.update(arg.arg for arg in (args.vararg, args.kwarg) if arg is not None)
|
|
241
|
+
return bound
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _has_wildcard_import(context: PythonFileContext) -> bool:
|
|
245
|
+
return any(alias.name == "*" for node in context.nodes(ast.ImportFrom) for alias in node.names)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _uncertain_annotation(
|
|
249
|
+
annotation: ast.expr | None,
|
|
250
|
+
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
251
|
+
context: PythonFileContext,
|
|
252
|
+
semantics: AnnotationSemantics,
|
|
253
|
+
) -> bool:
|
|
254
|
+
if annotation is None:
|
|
255
|
+
return False
|
|
256
|
+
if _has_wildcard_import(context):
|
|
257
|
+
return True
|
|
258
|
+
if context.tree is not None:
|
|
259
|
+
unknown = scope_bound_names(context.tree.body) - set(semantics.imports.bindings) - set(semantics.aliases)
|
|
260
|
+
if _uses_names(annotation, semantics) & unknown:
|
|
261
|
+
return True
|
|
262
|
+
return _shadowed_annotation(annotation, function, context, semantics)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _type_parameter_names(owner: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef) -> set[str]:
|
|
266
|
+
return {
|
|
267
|
+
parameter.name
|
|
268
|
+
for parameter in owner.type_params
|
|
269
|
+
if isinstance(parameter, (ast.TypeVar, ast.TypeVarTuple, ast.ParamSpec))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _uses_names(expression: ast.expr | None, semantics: AnnotationSemantics) -> set[str]:
|
|
274
|
+
parsed = semantics.parse(expression)
|
|
275
|
+
return {node.id for node in walk(parsed) if isinstance(node, ast.Name)} if parsed is not None else set()
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _known_decorator(
|
|
279
|
+
decorator: ast.expr,
|
|
280
|
+
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
281
|
+
context: PythonFileContext,
|
|
282
|
+
semantics: AnnotationSemantics,
|
|
283
|
+
) -> str | None:
|
|
284
|
+
if _has_wildcard_import(context):
|
|
285
|
+
return None
|
|
286
|
+
if _shadowed_annotation(decorator, function, context, semantics):
|
|
287
|
+
return None
|
|
288
|
+
if isinstance(decorator, ast.Name) and decorator.id in {"classmethod", "staticmethod"}:
|
|
289
|
+
if context.tree is not None and decorator.id in scope_bound_names(context.tree.body):
|
|
290
|
+
return None
|
|
291
|
+
if semantics.imports.builtin_is_unshadowed(decorator.id):
|
|
292
|
+
return "builtins." + decorator.id
|
|
293
|
+
return semantics.imports.resolved_qualified_name(decorator)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _unknown_decorator(
|
|
297
|
+
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
298
|
+
context: PythonFileContext,
|
|
299
|
+
semantics: AnnotationSemantics,
|
|
300
|
+
) -> bool:
|
|
301
|
+
return any(
|
|
302
|
+
_known_decorator(decorator, function, context, semantics) not in _KNOWN_DECORATORS
|
|
303
|
+
for decorator in function.decorator_list
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _overload_implementations(
|
|
308
|
+
context: PythonFileContext,
|
|
309
|
+
semantics: AnnotationSemantics,
|
|
310
|
+
) -> set[ast.FunctionDef | ast.AsyncFunctionDef]:
|
|
311
|
+
families: dict[tuple[ast.AST | None, str], list[ast.FunctionDef | ast.AsyncFunctionDef]] = {}
|
|
312
|
+
implementations: set[ast.FunctionDef | ast.AsyncFunctionDef] = set()
|
|
313
|
+
events = context.nodes(ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Name, ast.Import, ast.ImportFrom)
|
|
314
|
+
for node in sorted(events, key=lambda item: (item.lineno, item.col_offset)):
|
|
315
|
+
scope = _lexical_scope(node, context)
|
|
316
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
317
|
+
key = scope, node.name
|
|
318
|
+
if _signature_declaration(node, context, semantics):
|
|
319
|
+
families.setdefault(key, []).append(node)
|
|
320
|
+
else:
|
|
321
|
+
family = families.pop(key, [])
|
|
322
|
+
if _precise_family(family, semantics):
|
|
323
|
+
implementations.add(node)
|
|
324
|
+
else:
|
|
325
|
+
for name in _rebound_names(node):
|
|
326
|
+
families.pop((scope, name), None)
|
|
327
|
+
return implementations
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _lexical_scope(node: ast.AST, context: PythonFileContext) -> ast.AST | None:
|
|
331
|
+
parent = context.parents.get(node)
|
|
332
|
+
while parent is not None and not isinstance(
|
|
333
|
+
parent, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)
|
|
334
|
+
):
|
|
335
|
+
parent = context.parents.get(parent)
|
|
336
|
+
return parent
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _precise_family(family: list[ast.FunctionDef | ast.AsyncFunctionDef], semantics: AnnotationSemantics) -> bool:
|
|
340
|
+
return len(family) >= _MIN_OVERLOAD_DECLARATIONS and all(
|
|
341
|
+
f.args.kwarg is None or not _is_broad(f.args.kwarg.annotation, semantics) for f in family
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _rebound_names(node: ast.AST) -> set[str]:
|
|
346
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, (ast.Store, ast.Del)):
|
|
347
|
+
return {node.id}
|
|
348
|
+
if isinstance(node, ast.ClassDef):
|
|
349
|
+
return {node.name}
|
|
350
|
+
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
351
|
+
return scope_bound_names([node])
|
|
352
|
+
return set()
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _signature_declaration(
|
|
356
|
+
function: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
357
|
+
context: PythonFileContext,
|
|
358
|
+
semantics: AnnotationSemantics,
|
|
359
|
+
) -> bool:
|
|
360
|
+
decorators = {_known_decorator(decorator, function, context, semantics) for decorator in function.decorator_list}
|
|
361
|
+
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
|