sarj-python-lint 0.87.1__tar.gz → 0.87.3__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-0.87.1 → sarj_python_lint-0.87.3}/PKG-INFO +1 -1
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/pyproject.toml +1 -1
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rule_base.py +3 -0
- sarj_python_lint-0.87.3/src/sarj_python_lint/rules/_ast_position.py +21 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/fakes_in_shared_location.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +13 -6
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/iac_source_coupled_test.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/mock_without_spec.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/named_record_at_boundaries.py +2 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_any_mapping_types.py +2 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +3 -2
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +15 -7
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +2 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_redundant_literal_description.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +2 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_restated_comment.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_typed_doc_sections.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_unused_value_marker.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/over_mocked_test.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +2 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +4 -2
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_library_fake.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +15 -7
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +14 -6
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_module_level_constant.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +4 -2
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +16 -8
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_or_pattern.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +2 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +12 -4
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +2 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_str_enum.py +12 -5
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +2 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +2 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +2 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/redundant_docstring.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/repeated_static_call_cases.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/require_port_for_service.py +27 -20
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +82 -44
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +32 -16
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +16 -7
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/typed_error_reasons.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/unused_test_factory_option.py +1 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/.gitignore +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/LICENSE +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/README.md +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/require_typed_http_test_response.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/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: 0.87.
|
|
3
|
+
Version: 0.87.3
|
|
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/
|
|
@@ -155,6 +155,7 @@ class RuleDocumentation:
|
|
|
155
155
|
rationale: str
|
|
156
156
|
remediation: str
|
|
157
157
|
category: RuleCategory
|
|
158
|
+
default_level: Severity = Severity.ERROR
|
|
158
159
|
autofix: AutofixPolicy = AutofixPolicy.NONE
|
|
159
160
|
aliases: tuple[str, ...] = ()
|
|
160
161
|
limitations: tuple[str, ...] = ()
|
|
@@ -207,6 +208,7 @@ class NativeRuleSpec:
|
|
|
207
208
|
rationale: str
|
|
208
209
|
remediation: str
|
|
209
210
|
category: RuleCategory
|
|
211
|
+
default_level: Severity
|
|
210
212
|
autofix: AutofixPolicy
|
|
211
213
|
aliases: tuple[str, ...]
|
|
212
214
|
limitations: tuple[str, ...]
|
|
@@ -283,6 +285,7 @@ class Rule(ABC):
|
|
|
283
285
|
rationale=authored.rationale,
|
|
284
286
|
remediation=authored.remediation,
|
|
285
287
|
category=authored.category,
|
|
288
|
+
default_level=authored.default_level,
|
|
286
289
|
autofix=authored.autofix,
|
|
287
290
|
aliases=authored.aliases,
|
|
288
291
|
limitations=authored.limitations,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
import ast
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, order=True, slots=True)
|
|
12
|
+
class AstPosition:
|
|
13
|
+
line: int
|
|
14
|
+
column: int
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def ast_position(node: ast.AST, *, missing: int) -> AstPosition:
|
|
18
|
+
return AstPosition(
|
|
19
|
+
line=getattr(node, "lineno", missing),
|
|
20
|
+
column=getattr(node, "col_offset", missing),
|
|
21
|
+
)
|
|
@@ -264,6 +264,7 @@ class ComplexPostgresQueryRequiresArchitectureReview(Rule):
|
|
|
264
264
|
id: str = "complex-postgres-query-requires-architecture-review"
|
|
265
265
|
code: str = "SARJ437"
|
|
266
266
|
documentation = RuleDocumentation(
|
|
267
|
+
default_level=Severity.WARNING,
|
|
267
268
|
summary="Complex executable PostgreSQL query shapes require architecture review.",
|
|
268
269
|
rationale=(
|
|
269
270
|
"Join-heavy, deeply staged, or wide joined reads can obscure cardinality, bounds, ordering, and locking "
|
|
@@ -50,6 +50,7 @@ class FakesInSharedLocation(Rule):
|
|
|
50
50
|
id = "fakes-in-shared-location"
|
|
51
51
|
code = "SARJ428"
|
|
52
52
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
53
|
+
default_level=Severity.WARNING,
|
|
53
54
|
summary="Review named top-level test doubles for shared-support ownership unless they are intentionally scenario-local.",
|
|
54
55
|
rationale=(
|
|
55
56
|
"Reusable doubles hidden in an individual test module are difficult to discover and are often recreated. "
|
|
@@ -73,6 +73,7 @@ class FastapiExplicitOpenapiContract(Rule):
|
|
|
73
73
|
id: str = "fastapi-explicit-openapi-contract"
|
|
74
74
|
code: str = "SARJ094"
|
|
75
75
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
76
|
+
default_level=Severity.WARNING,
|
|
76
77
|
summary="Visible FastAPI operations pin locally reviewable metadata and avoid statically provable OpenAPI gaps.",
|
|
77
78
|
rationale=(
|
|
78
79
|
"FastAPI can infer valid schemas and default statuses. This stricter organizational policy pins locally "
|
|
@@ -674,12 +675,12 @@ def _check_direct_responses(
|
|
|
674
675
|
routes: tuple[Route, ...],
|
|
675
676
|
index: FastapiIndex,
|
|
676
677
|
) -> list[_Finding]:
|
|
677
|
-
|
|
678
|
+
direct = _direct_response_statuses(function, index)
|
|
678
679
|
findings = [
|
|
679
680
|
_Finding(node, f"[status] {status} is outside the HTTP status range 100..599.")
|
|
680
|
-
for status, node in invalid.items()
|
|
681
|
+
for status, node in direct.invalid.items()
|
|
681
682
|
]
|
|
682
|
-
if not statuses:
|
|
683
|
+
if not direct.statuses:
|
|
683
684
|
return findings
|
|
684
685
|
for route in _operations(routes):
|
|
685
686
|
responses = _route_response_nodes(route)
|
|
@@ -689,7 +690,7 @@ def _check_direct_responses(
|
|
|
689
690
|
status for node in responses if isinstance(node, ast.Dict) for status in _response_codes(node, index)
|
|
690
691
|
}
|
|
691
692
|
primary = _status_code(route.keywords.get("status_code"), index)
|
|
692
|
-
missing = sorted(statuses - documented - ({primary} if primary is not None else set()))
|
|
693
|
+
missing = sorted(direct.statuses - documented - ({primary} if primary is not None else set()))
|
|
693
694
|
if missing:
|
|
694
695
|
findings.append(
|
|
695
696
|
_Finding(
|
|
@@ -700,9 +701,15 @@ def _check_direct_responses(
|
|
|
700
701
|
return findings
|
|
701
702
|
|
|
702
703
|
|
|
704
|
+
@dataclass(frozen=True, slots=True)
|
|
705
|
+
class _DirectResponseStatuses:
|
|
706
|
+
statuses: set[int]
|
|
707
|
+
invalid: dict[int, ast.Call]
|
|
708
|
+
|
|
709
|
+
|
|
703
710
|
def _direct_response_statuses(
|
|
704
711
|
function: ast.FunctionDef | ast.AsyncFunctionDef, index: FastapiIndex
|
|
705
|
-
) ->
|
|
712
|
+
) -> _DirectResponseStatuses:
|
|
706
713
|
statuses: set[int] = set()
|
|
707
714
|
invalid: dict[int, ast.Call] = {}
|
|
708
715
|
stack: list[tuple[ast.AST, bool]] = [(node, False) for node in function.body]
|
|
@@ -721,7 +728,7 @@ def _direct_response_statuses(
|
|
|
721
728
|
else:
|
|
722
729
|
invalid.setdefault(status, call)
|
|
723
730
|
stack.extend((child, catches_http_exception) for child in children(current))
|
|
724
|
-
return statuses, invalid
|
|
731
|
+
return _DirectResponseStatuses(statuses=statuses, invalid=invalid)
|
|
725
732
|
|
|
726
733
|
|
|
727
734
|
def _extend_response_try(
|
|
@@ -32,6 +32,7 @@ class IacSourceCoupledTest(Rule):
|
|
|
32
32
|
id = "iac-source-coupled-test"
|
|
33
33
|
code = "SARJ412"
|
|
34
34
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
35
|
+
default_level=Severity.WARNING,
|
|
35
36
|
summary="Test uses raw Terraform/HCL text as an infrastructure-behavior oracle.",
|
|
36
37
|
rationale=(
|
|
37
38
|
"Substring and regex checks can pass on comments, formatting, or unreachable Terraform configuration "
|
{sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/mock_without_spec.py
RENAMED
|
@@ -121,6 +121,7 @@ class MockWithoutSpec(Rule):
|
|
|
121
121
|
id: str = "mock-without-spec"
|
|
122
122
|
code: str = "SARJ040"
|
|
123
123
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
124
|
+
default_level=Severity.WARNING,
|
|
124
125
|
summary="Unrestricted mock permits attributes outside the collaborator contract.",
|
|
125
126
|
rationale=(
|
|
126
127
|
"A mock without a spec permits removed or misspelled attributes; a callable mock without autospec may "
|
|
@@ -13,6 +13,7 @@ from sarj_python_lint.rule_base import (
|
|
|
13
13
|
RuleCategory,
|
|
14
14
|
RuleDocumentation,
|
|
15
15
|
RuleExample,
|
|
16
|
+
Severity,
|
|
16
17
|
is_suppressed,
|
|
17
18
|
parse_or_none,
|
|
18
19
|
)
|
|
@@ -42,6 +43,7 @@ class NamedRecordAtBoundaries(Rule):
|
|
|
42
43
|
id: str = "named-record-at-boundaries"
|
|
43
44
|
code: str = "SARJ008"
|
|
44
45
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
46
|
+
default_level=Severity.WARNING,
|
|
45
47
|
summary="Public Python API returns an unnamed fixed-shape record.",
|
|
46
48
|
rationale="A named record makes field types and required keys explicit to callers and static tooling.",
|
|
47
49
|
remediation="Define and return a `TypedDict`, Pydantic model, or frozen dataclass for the fixed record shape.",
|
|
@@ -204,6 +204,7 @@ class NegativeOnlyHttpStatusAssertion(Rule):
|
|
|
204
204
|
id = "negative-only-http-status-assertion"
|
|
205
205
|
code = "SARJ408"
|
|
206
206
|
documentation = RuleDocumentation(
|
|
207
|
+
default_level=Severity.WARNING,
|
|
207
208
|
summary="HTTP test assertion only excludes a server error instead of identifying the intended response.",
|
|
208
209
|
rationale=(
|
|
209
210
|
"Authentication, routing, validation, and domain failures can all replace the intended response while "
|
|
@@ -152,6 +152,7 @@ class NoAnalyticalAggregationInPostgresStore(Rule):
|
|
|
152
152
|
id: str = "no-analytical-aggregation-in-postgres-store"
|
|
153
153
|
code: str = "SARJ020"
|
|
154
154
|
documentation = RuleDocumentation(
|
|
155
|
+
default_level=Severity.WARNING,
|
|
155
156
|
summary="Potentially analytical PostgreSQL store queries require review.",
|
|
156
157
|
rationale=(
|
|
157
158
|
"Unbounded reporting and statistical scans can compete with transactional reads. "
|
|
@@ -13,6 +13,7 @@ from sarj_python_lint.rule_base import (
|
|
|
13
13
|
RuleCategory,
|
|
14
14
|
RuleDocumentation,
|
|
15
15
|
RuleExample,
|
|
16
|
+
Severity,
|
|
16
17
|
is_suppressed,
|
|
17
18
|
)
|
|
18
19
|
from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics, scope_bound_names
|
|
@@ -37,6 +38,7 @@ class NoAnyMappingTypes(Rule):
|
|
|
37
38
|
id = "no-any-mapping-types"
|
|
38
39
|
code = "SARJ447"
|
|
39
40
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
41
|
+
default_level=Severity.WARNING,
|
|
40
42
|
summary="String-keyed mapping types must not erase their values with `Any`.",
|
|
41
43
|
rationale=(
|
|
42
44
|
"An `Any`-valued mapping disables checking transitively and conceals whether the value is a fixed record, "
|
|
@@ -20,6 +20,7 @@ from sarj_python_lint.rule_base import (
|
|
|
20
20
|
is_suppressed,
|
|
21
21
|
parse_or_none,
|
|
22
22
|
)
|
|
23
|
+
from sarj_python_lint.rules._ast_position import AstPosition, ast_position
|
|
23
24
|
from sarj_python_lint.rules._imports import ImportIndex
|
|
24
25
|
from sarj_python_lint.rules._paths import is_generated, is_test_path, is_test_support_path
|
|
25
26
|
|
|
@@ -954,8 +955,8 @@ def _target_names(node: ast.expr) -> set[str]:
|
|
|
954
955
|
return set()
|
|
955
956
|
|
|
956
957
|
|
|
957
|
-
def _position(node: ast.AST) ->
|
|
958
|
-
return
|
|
958
|
+
def _position(node: ast.AST) -> AstPosition:
|
|
959
|
+
return ast_position(node, missing=-1)
|
|
959
960
|
|
|
960
961
|
|
|
961
962
|
def _scope_nodes(statements: Iterable[ast.stmt], *, stop_rebinding: str | None = None) -> Iterator[ast.AST]:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
4
5
|
from pathlib import PurePosixPath
|
|
5
6
|
import re
|
|
6
7
|
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
@@ -26,6 +27,13 @@ if TYPE_CHECKING:
|
|
|
26
27
|
|
|
27
28
|
_RANDOM_UUID_RE = re.compile(r"\b(?:gen_random_uuid|uuid_generate_v4)\s*\(", re.IGNORECASE)
|
|
28
29
|
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class _IdentifierEnd:
|
|
33
|
+
end: int
|
|
34
|
+
after_whitespace: int
|
|
35
|
+
|
|
36
|
+
|
|
29
37
|
# Require SQL structure so prose that merely names the function stays valid.
|
|
30
38
|
_SQL_SHAPE_RE = re.compile(
|
|
31
39
|
r"\b(?:CREATE|ALTER|INSERT|UPDATE|SELECT|DEFAULT|VALUES)\b",
|
|
@@ -132,12 +140,12 @@ def _inside_uuidv7_builder(sql: str, position: int) -> bool:
|
|
|
132
140
|
index = 0
|
|
133
141
|
while index < position:
|
|
134
142
|
if sql[index].isalpha() or sql[index] == "_":
|
|
135
|
-
|
|
136
|
-
if
|
|
137
|
-
calls.append(sql[index:end].lower())
|
|
138
|
-
index =
|
|
143
|
+
identifier = _identifier_end(sql, index, position)
|
|
144
|
+
if identifier.after_whitespace < position and sql[identifier.after_whitespace] == "(":
|
|
145
|
+
calls.append(sql[index : identifier.end].lower())
|
|
146
|
+
index = identifier.after_whitespace + 1
|
|
139
147
|
continue
|
|
140
|
-
index = end
|
|
148
|
+
index = identifier.end
|
|
141
149
|
continue
|
|
142
150
|
if sql[index] == "(":
|
|
143
151
|
calls.append(None)
|
|
@@ -147,11 +155,11 @@ def _inside_uuidv7_builder(sql: str, position: int) -> bool:
|
|
|
147
155
|
return any(call in _UUIDV7_BUILDERS for call in calls)
|
|
148
156
|
|
|
149
157
|
|
|
150
|
-
def _identifier_end(sql: str, index: int, position: int) ->
|
|
158
|
+
def _identifier_end(sql: str, index: int, position: int) -> _IdentifierEnd:
|
|
151
159
|
end = index + 1
|
|
152
160
|
while end < position and (sql[end].isalnum() or sql[end] == "_"):
|
|
153
161
|
end += 1
|
|
154
162
|
after = end
|
|
155
163
|
while after < position and sql[after].isspace():
|
|
156
164
|
after += 1
|
|
157
|
-
return end, after
|
|
165
|
+
return _IdentifierEnd(end=end, after_whitespace=after)
|
|
@@ -13,6 +13,7 @@ from sarj_python_lint.rule_base import (
|
|
|
13
13
|
RuleCategory,
|
|
14
14
|
RuleDocumentation,
|
|
15
15
|
RuleExample,
|
|
16
|
+
Severity,
|
|
16
17
|
parse_or_none,
|
|
17
18
|
)
|
|
18
19
|
from sarj_python_lint.rules._paths import is_generated, is_test_path, is_test_support_path
|
|
@@ -30,6 +31,7 @@ class NoRawConnectionInTests(Rule):
|
|
|
30
31
|
id = "no-raw-connection-in-tests"
|
|
31
32
|
code = "SARJ429"
|
|
32
33
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
34
|
+
default_level=Severity.WARNING,
|
|
33
35
|
summary="Do not acquire raw database connections in tests.",
|
|
34
36
|
rationale=(
|
|
35
37
|
"Tests that reach through a pool couple assertions and setup to persistence internals, bypass the "
|
|
@@ -112,6 +112,7 @@ class NoRawSourceTextTestOracle(Rule):
|
|
|
112
112
|
id = "no-raw-source-text-test-oracle"
|
|
113
113
|
code = "SARJ402"
|
|
114
114
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
115
|
+
default_level=Severity.WARNING,
|
|
115
116
|
summary="Test uses raw text from a source-like project path as its oracle.",
|
|
116
117
|
rationale="Substring and regex checks can pass on comments or unreachable configuration and fail after behavior-preserving formatting changes.",
|
|
117
118
|
remediation=(
|
|
@@ -44,6 +44,7 @@ class NoRedundantLiteralDescription(Rule):
|
|
|
44
44
|
id = "no-restated-closed-domain-description"
|
|
45
45
|
code = "SARJ423"
|
|
46
46
|
documentation = RuleDocumentation(
|
|
47
|
+
default_level=Severity.WARNING,
|
|
47
48
|
summary="Do not restate a string Literal or local string Enum domain in its Pydantic description.",
|
|
48
49
|
rationale=(
|
|
49
50
|
"Pydantic already publishes literal and enum domains in JSON Schema; duplicate must-be prose can "
|
|
@@ -14,6 +14,7 @@ from sarj_python_lint.rule_base import (
|
|
|
14
14
|
RuleCategory,
|
|
15
15
|
RuleDocumentation,
|
|
16
16
|
RuleExample,
|
|
17
|
+
Severity,
|
|
17
18
|
parse_or_none,
|
|
18
19
|
)
|
|
19
20
|
from sarj_python_lint.rules._imports import ImportIndex
|
|
@@ -33,6 +34,7 @@ class NoRedundantModuleAliasExports(Rule):
|
|
|
33
34
|
id: str = "no-redundant-module-alias-exports"
|
|
34
35
|
code: str = "SARJ440"
|
|
35
36
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
37
|
+
default_level=Severity.WARNING,
|
|
36
38
|
summary="Do not manufacture public APIs from private names or replace the current module.",
|
|
37
39
|
rationale=(
|
|
38
40
|
"Binding a public name to a private implementation creates two names for one API and disguises the "
|
|
@@ -253,6 +253,7 @@ class NoRestatedComment(Rule):
|
|
|
253
253
|
id: str = "no-restated-comment"
|
|
254
254
|
code: str = "SARJ049"
|
|
255
255
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
256
|
+
default_level=Severity.WARNING,
|
|
256
257
|
summary="Short standalone comment lexically restates the immediately following simple action.",
|
|
257
258
|
rationale=(
|
|
258
259
|
"When a comment adds no rationale, scope, ordering, or constraint, it duplicates code and can become stale. "
|
|
@@ -28,6 +28,7 @@ class NoTypedDocSections(Rule):
|
|
|
28
28
|
id = "no-docstring-type-restatement"
|
|
29
29
|
code = "SARJ092"
|
|
30
30
|
documentation = RuleDocumentation(
|
|
31
|
+
default_level=Severity.WARNING,
|
|
31
32
|
summary="A docstring type label repeats an annotation from the fully typed signature.",
|
|
32
33
|
rationale=(
|
|
33
34
|
"A repeated type spelling can drift from the annotation and obscures the behavioral contract that only prose "
|
|
@@ -29,6 +29,7 @@ class NoUnusedValueMarker(Rule):
|
|
|
29
29
|
id = "no-unused-value-marker"
|
|
30
30
|
code = "SARJ452"
|
|
31
31
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
32
|
+
default_level=Severity.WARNING,
|
|
32
33
|
summary="Do not use standalone assignments to `_` to discard values.",
|
|
33
34
|
rationale=(
|
|
34
35
|
"A standalone `_ = value` assignment adds a binding without clarifying intent. A side-effecting call can "
|
|
@@ -114,6 +114,7 @@ class NoWholeRequestResponsePayloadInLog(Rule):
|
|
|
114
114
|
id: str = "no-whole-request-response-payload-in-log"
|
|
115
115
|
code: str = "SARJ436"
|
|
116
116
|
documentation = RuleDocumentation(
|
|
117
|
+
default_level=Severity.WARNING,
|
|
117
118
|
summary="Whole request or response payloads passed to logging calls require review.",
|
|
118
119
|
rationale=(
|
|
119
120
|
"Request and response bodies often contain personal, financial, or echoed authentication data, while "
|
|
@@ -56,6 +56,7 @@ class OpaqueParametrizeCaseNeedsId(Rule):
|
|
|
56
56
|
id: str = "opaque-parametrize-case-needs-id"
|
|
57
57
|
code: str = "SARJ042"
|
|
58
58
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
59
|
+
default_level=Severity.WARNING,
|
|
59
60
|
summary="Opaque static pytest parameter cases rely on argument-name-and-index fallback IDs.",
|
|
60
61
|
rationale="Fallback IDs such as `payload0` are hard to diagnose and silently change when the table is reordered.",
|
|
61
62
|
remediation=(
|
{sarj_python_lint-0.87.1 → sarj_python_lint-0.87.3}/src/sarj_python_lint/rules/over_mocked_test.py
RENAMED
|
@@ -179,6 +179,7 @@ class OverMockedTest(Rule):
|
|
|
179
179
|
id: str = "over-mocked-test"
|
|
180
180
|
code: str = "SARJ062"
|
|
181
181
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
182
|
+
default_level=Severity.WARNING,
|
|
182
183
|
summary="Tests should not use more than five independently rooted test doubles or collaborator substitutions.",
|
|
183
184
|
rationale="Broad double setup obscures the behavior under test and often couples tests to implementation wiring.",
|
|
184
185
|
remediation=(
|
|
@@ -18,6 +18,7 @@ from sarj_python_lint.rule_base import (
|
|
|
18
18
|
RuleCategory,
|
|
19
19
|
RuleDocumentation,
|
|
20
20
|
RuleExample,
|
|
21
|
+
Severity,
|
|
21
22
|
is_suppressed,
|
|
22
23
|
parse_or_none,
|
|
23
24
|
)
|
|
@@ -77,6 +78,7 @@ class PreferCollectionComprehension(Rule):
|
|
|
77
78
|
id = "prefer-collection-comprehension"
|
|
78
79
|
code = "SARJ430"
|
|
79
80
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
81
|
+
default_level=Severity.WARNING,
|
|
80
82
|
summary="Single-purpose fresh collection builder loop — prefer a direct comprehension.",
|
|
81
83
|
rationale=(
|
|
82
84
|
"An empty collection followed by a loop whose only behavior is one projection or filtered insertion "
|
|
@@ -94,6 +94,7 @@ class PreferConstantTimeSecretCompare(Rule):
|
|
|
94
94
|
id: str = "prefer-constant-time-secret-compare"
|
|
95
95
|
code: str = "SARJ011"
|
|
96
96
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
97
|
+
default_level=Severity.WARNING,
|
|
97
98
|
summary="Externally supplied authenticators are compared with timing-sensitive equality.",
|
|
98
99
|
rationale=(
|
|
99
100
|
"Ordinary equality may short-circuit based on matching content when attacker-controlled credentials are "
|
|
@@ -120,6 +120,7 @@ class PreferFstringOverConcat(Rule):
|
|
|
120
120
|
id: str = "prefer-fstring-over-concat"
|
|
121
121
|
code: str = "SARJ068"
|
|
122
122
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
123
|
+
default_level=Severity.WARNING,
|
|
123
124
|
summary="Prefer f-strings for short human-readable interpolation when they make the result clearer.",
|
|
124
125
|
rationale="For prose-like strings, f-strings keep interpolated values and surrounding text visible together.",
|
|
125
126
|
remediation=(
|
|
@@ -73,6 +73,7 @@ class PreferImmutableModuleConstant(Rule):
|
|
|
73
73
|
id: str = "prefer-immutable-module-constant"
|
|
74
74
|
code: str = "SARJ096"
|
|
75
75
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
76
|
+
default_level=Severity.WARNING,
|
|
76
77
|
summary=("Nonempty uppercase module collections allow top-level membership or keys to change at runtime."),
|
|
77
78
|
rationale=(
|
|
78
79
|
"A constant-looking collection can expose process-wide top-level mutation even when callers intend it as a "
|
|
@@ -16,6 +16,7 @@ from sarj_python_lint.rule_base import (
|
|
|
16
16
|
Severity,
|
|
17
17
|
parse_or_none,
|
|
18
18
|
)
|
|
19
|
+
from sarj_python_lint.rules._ast_position import AstPosition, ast_position
|
|
19
20
|
from sarj_python_lint.rules._imports import ImportIndex
|
|
20
21
|
from sarj_python_lint.rules._paths import is_generated, is_test_path
|
|
21
22
|
|
|
@@ -36,6 +37,7 @@ class PreferInjectedDependencyOverMonkeypatch(Rule):
|
|
|
36
37
|
id: str = "prefer-injected-dependency-over-monkeypatch"
|
|
37
38
|
code: str = "SARJ445"
|
|
38
39
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
40
|
+
default_level=Severity.WARNING,
|
|
39
41
|
summary="Tests should inject dependencies instead of replacing attributes through ambient patching.",
|
|
40
42
|
rationale=(
|
|
41
43
|
"Attribute patching hides collaborators and configuration behind ambient module or object state, coupling "
|
|
@@ -250,10 +252,10 @@ def _parameter_rebound_before(
|
|
|
250
252
|
return any(handle in _bound_targets(node) and _position(node) < use_position for node in nodes)
|
|
251
253
|
|
|
252
254
|
|
|
253
|
-
def _position(node: ast.AST) ->
|
|
255
|
+
def _position(node: ast.AST) -> AstPosition:
|
|
254
256
|
if isinstance(node, ast.withitem):
|
|
255
257
|
return _position(node.context_expr)
|
|
256
|
-
return (
|
|
258
|
+
return ast_position(node, missing=0)
|
|
257
259
|
|
|
258
260
|
|
|
259
261
|
def _direct_aliases(nodes: tuple[ast.AST, ...], handles: set[str], parameters: set[str]) -> set[str]:
|
|
@@ -341,6 +341,7 @@ class PreferLibraryFake(Rule):
|
|
|
341
341
|
id: str = "prefer-library-fake"
|
|
342
342
|
code: str = "SARJ059"
|
|
343
343
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
344
|
+
default_level=Severity.WARNING,
|
|
344
345
|
summary="Prefer a maintained fake, emulator, recorder, or test service for substantial third-party protocols.",
|
|
345
346
|
rationale=(
|
|
346
347
|
"A substantial hand-written protocol model can drift from supported request, state, and error behavior."
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
4
5
|
from pathlib import PurePosixPath
|
|
5
6
|
from typing import TYPE_CHECKING, ClassVar, NamedTuple, final, override
|
|
6
7
|
|
|
@@ -85,6 +86,7 @@ class PreferMatchExceptionDispatch(Rule):
|
|
|
85
86
|
id: str = "prefer-match-exception-dispatch"
|
|
86
87
|
code: str = "SARJ453"
|
|
87
88
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
89
|
+
default_level=Severity.WARNING,
|
|
88
90
|
summary="Prefer guarded match/case for refined exception type dispatch.",
|
|
89
91
|
rationale=(
|
|
90
92
|
"A long exception classifier that repeats isinstance checks and then refines one exception type hides "
|
|
@@ -215,17 +217,23 @@ def _classifier_starts(
|
|
|
215
217
|
for statements in _nested_statement_lists(body):
|
|
216
218
|
index = 0
|
|
217
219
|
while index < len(statements):
|
|
218
|
-
|
|
219
|
-
branches = _validated_branches(candidates, subject, imports)
|
|
220
|
-
if branches is not None and _is_classifier(branches, statements, cursor):
|
|
220
|
+
adjacent = _adjacent_bare_ifs(statements, index)
|
|
221
|
+
branches = _validated_branches(adjacent.candidates, subject, imports)
|
|
222
|
+
if branches is not None and _is_classifier(branches, statements, adjacent.cursor):
|
|
221
223
|
findings.append(branches[0][0])
|
|
222
|
-
index = cursor + 1
|
|
224
|
+
index = adjacent.cursor + 1
|
|
223
225
|
else:
|
|
224
|
-
index = max(cursor, index + 1)
|
|
226
|
+
index = max(adjacent.cursor, index + 1)
|
|
225
227
|
return tuple(findings)
|
|
226
228
|
|
|
227
229
|
|
|
228
|
-
|
|
230
|
+
@dataclass(frozen=True, slots=True)
|
|
231
|
+
class _AdjacentBareIfs:
|
|
232
|
+
candidates: list[ast.If]
|
|
233
|
+
cursor: int
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _adjacent_bare_ifs(statements: list[ast.stmt], start: int) -> _AdjacentBareIfs:
|
|
229
237
|
cursor = start
|
|
230
238
|
candidates: list[ast.If] = []
|
|
231
239
|
while cursor < len(statements):
|
|
@@ -234,7 +242,7 @@ def _adjacent_bare_ifs(statements: list[ast.stmt], start: int) -> tuple[list[ast
|
|
|
234
242
|
break
|
|
235
243
|
candidates.append(candidate)
|
|
236
244
|
cursor += 1
|
|
237
|
-
return candidates, cursor
|
|
245
|
+
return _AdjacentBareIfs(candidates=candidates, cursor=cursor)
|
|
238
246
|
|
|
239
247
|
|
|
240
248
|
def _validated_branches(
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
4
|
from collections import Counter
|
|
5
|
+
from dataclasses import dataclass
|
|
5
6
|
from pathlib import PurePosixPath
|
|
6
7
|
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
7
8
|
|
|
@@ -37,6 +38,7 @@ class PreferMatchValueDispatch(Rule):
|
|
|
37
38
|
id: str = "prefer-match-value-dispatch"
|
|
38
39
|
code: str = "SARJ439"
|
|
39
40
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
41
|
+
default_level=Severity.WARNING,
|
|
40
42
|
summary="Prefer match/case for repeated dispatch on one value with a fallback.",
|
|
41
43
|
rationale="A single dispatch subject makes distinct value cases and their fallback easier to review.",
|
|
42
44
|
remediation=(
|
|
@@ -164,12 +166,12 @@ class PreferMatchValueDispatch(Rule):
|
|
|
164
166
|
for node in ast.walk(tree):
|
|
165
167
|
if not isinstance(node, ast.If) or id(node) in continuations:
|
|
166
168
|
continue
|
|
167
|
-
|
|
168
|
-
if len(branches) < _MIN_TESTS or not
|
|
169
|
+
chain = _elif_chain(node, lines, continuations)
|
|
170
|
+
if len(chain.branches) < _MIN_TESTS or not chain.last.orelse:
|
|
169
171
|
continue
|
|
170
|
-
subject = _dispatch_subject(branches, constants)
|
|
172
|
+
subject = _dispatch_subject(chain.branches, constants)
|
|
171
173
|
if subject is None:
|
|
172
|
-
subject = _expanded_dispatch_subject(branches, constants)
|
|
174
|
+
subject = _expanded_dispatch_subject(chain.branches, constants)
|
|
173
175
|
if subject is not None:
|
|
174
176
|
candidates.append((node, subject))
|
|
175
177
|
for branches in _terminal_sibling_dispatches(tree):
|
|
@@ -368,7 +370,13 @@ def _subject(expression: ast.expr) -> str | None:
|
|
|
368
370
|
return None
|
|
369
371
|
|
|
370
372
|
|
|
371
|
-
|
|
373
|
+
@dataclass(frozen=True, slots=True)
|
|
374
|
+
class _ElifChain:
|
|
375
|
+
branches: list[ast.If]
|
|
376
|
+
last: ast.If
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _elif_chain(node: ast.If, lines: list[str], continuations: set[int]) -> _ElifChain:
|
|
372
380
|
branches = [node]
|
|
373
381
|
current = node
|
|
374
382
|
while len(current.orelse) == 1:
|
|
@@ -378,7 +386,7 @@ def _elif_chain(node: ast.If, lines: list[str], continuations: set[int]) -> tupl
|
|
|
378
386
|
continuations.add(id(child))
|
|
379
387
|
branches.append(child)
|
|
380
388
|
current = child
|
|
381
|
-
return branches, current
|
|
389
|
+
return _ElifChain(branches=branches, last=current)
|
|
382
390
|
|
|
383
391
|
|
|
384
392
|
def _collect_terminal_siblings(field: list[ast.stmt], candidates: list[list[ast.If]]) -> None:
|
|
@@ -96,6 +96,7 @@ class PreferModuleLevelConstant(Rule):
|
|
|
96
96
|
id: str = "prefer-module-level-constant"
|
|
97
97
|
code: str = "SARJ039"
|
|
98
98
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
99
|
+
default_level=Severity.WARNING,
|
|
99
100
|
summary="Hoist repeatedly read static values when an immutable module representation preserves behavior.",
|
|
100
101
|
rationale=(
|
|
101
102
|
"Rebuilding a substantial static collection repeats allocation, while calling `re.compile` with a "
|
|
@@ -16,6 +16,7 @@ from sarj_python_lint.rule_base import (
|
|
|
16
16
|
Severity,
|
|
17
17
|
parse_or_none,
|
|
18
18
|
)
|
|
19
|
+
from sarj_python_lint.rules._ast_position import AstPosition, ast_position
|
|
19
20
|
from sarj_python_lint.rules._imports import ImportIndex
|
|
20
21
|
from sarj_python_lint.rules._paths import is_generated, is_test_path
|
|
21
22
|
|
|
@@ -43,6 +44,7 @@ class PreferMonkeypatchForProcessStateInTest(Rule):
|
|
|
43
44
|
id = "prefer-monkeypatch-for-process-state-in-test"
|
|
44
45
|
code = "SARJ446"
|
|
45
46
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
47
|
+
default_level=Severity.WARNING,
|
|
46
48
|
summary="Test mutates process-wide state without a restoring test scope.",
|
|
47
49
|
rationale=(
|
|
48
50
|
"The working directory and Python import registries are shared by every test in the process. A direct "
|
|
@@ -294,8 +296,8 @@ def _import_root_rebound_before(
|
|
|
294
296
|
)
|
|
295
297
|
|
|
296
298
|
|
|
297
|
-
def _position(node: ast.AST) ->
|
|
298
|
-
return (
|
|
299
|
+
def _position(node: ast.AST) -> AstPosition:
|
|
300
|
+
return ast_position(node, missing=0)
|
|
299
301
|
|
|
300
302
|
|
|
301
303
|
def _restored_mutations(
|