sarj-python-lint 0.78.0__tar.gz → 0.78.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/PKG-INFO +1 -1
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/pyproject.toml +1 -1
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_registry.py +31 -27
- sarj_python_lint-0.78.0/src/sarj_python_lint/rules/defect_xfail_requires_strict.py → sarj_python_lint-0.78.2/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +34 -8
- sarj_python_lint-0.78.2/src/sarj_python_lint/rules/fakes_in_shared_location.py +113 -0
- sarj_python_lint-0.78.0/src/sarj_python_lint/rules/fastapi_openapi_contract.py → sarj_python_lint-0.78.2/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +50 -13
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/iac_source_coupled_test.py +3 -2
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +196 -23
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/mock_without_spec.py +13 -5
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +69 -5
- sarj_python_lint-0.78.0/src/sarj_python_lint/rules/no_aggregation_in_store_query.py → sarj_python_lint-0.78.2/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +21 -5
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_comment_cruft.py +6 -6
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +73 -12
- sarj_python_lint-0.78.0/src/sarj_python_lint/rules/duplicated_override_docstring.py → sarj_python_lint-0.78.2/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +104 -30
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +58 -3
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_fastapi_on_event.py +54 -24
- sarj_python_lint-0.78.0/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py → sarj_python_lint-0.78.2/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +26 -14
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_first_party_private_import.py +91 -11
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +40 -7
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_generic_single_export_module.py +7 -5
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +24 -5
- sarj_python_lint-0.78.0/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py → sarj_python_lint-0.78.2/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +43 -12
- sarj_python_lint-0.78.2/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +154 -0
- sarj_python_lint-0.78.0/src/sarj_python_lint/rules/duplicate_test_body.py → sarj_python_lint-0.78.2/src/sarj_python_lint/rules/no_repeated_test_body.py +6 -5
- sarj_python_lint-0.78.2/src/sarj_python_lint/rules/prefer_collection_comprehension.py +441 -0
- sarj_python_lint-0.78.0/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py → sarj_python_lint-0.78.2/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +37 -13
- sarj_python_lint-0.78.0/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py → sarj_python_lint-0.78.2/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +21 -26
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/repeated_static_call_cases.py +2 -2
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/source_coupled_test.py +1 -1
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/stepdown.py +1 -4
- sarj_python_lint-0.78.0/src/sarj_python_lint/rules/get_delegates_to_get_many.py → sarj_python_lint-0.78.2/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +78 -29
- sarj_python_lint-0.78.0/src/sarj_python_lint/rules/created_at_order_requires_tiebreaker.py → sarj_python_lint-0.78.2/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +17 -16
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/.gitignore +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/LICENSE +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/README.md +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_long_comment.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_one_for_required_row.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/preserve_declared_nominal_id.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/preserve_enum_types.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/require_validated_row_factory.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/sql_requires_injected_pool_owner.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.78.0 → sarj_python_lint-0.78.2}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 0.78.
|
|
3
|
+
Version: 0.78.2
|
|
4
4
|
Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
|
|
5
5
|
Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
|
|
6
6
|
Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
|
|
@@ -3,50 +3,43 @@ from __future__ import annotations
|
|
|
3
3
|
from types import MappingProxyType
|
|
4
4
|
from typing import TYPE_CHECKING
|
|
5
5
|
|
|
6
|
-
from sarj_python_lint.rules.
|
|
7
|
-
CreatedAtOrderRequiresTiebreaker,
|
|
8
|
-
)
|
|
9
|
-
from sarj_python_lint.rules.defect_xfail_requires_strict import DefectXfailRequiresStrict
|
|
6
|
+
from sarj_python_lint.rules.defect_xfail_requires_explicit_strict import DefectXfailRequiresExplicitStrict
|
|
10
7
|
from sarj_python_lint.rules.docstring_args_restate_signature import (
|
|
11
8
|
DocstringArgsRestateSignature,
|
|
12
9
|
)
|
|
13
10
|
from sarj_python_lint.rules.docstring_returns_restate_signature import (
|
|
14
11
|
DocstringReturnsRestateSignature,
|
|
15
12
|
)
|
|
16
|
-
from sarj_python_lint.rules.
|
|
17
|
-
from sarj_python_lint.rules.
|
|
18
|
-
DuplicatedOverrideDocstring,
|
|
19
|
-
)
|
|
20
|
-
from sarj_python_lint.rules.fastapi_openapi_contract import FastapiOpenapiContract
|
|
21
|
-
from sarj_python_lint.rules.fixture_returns_bare_tuple import FixtureReturnsBareTuple
|
|
22
|
-
from sarj_python_lint.rules.get_delegates_to_get_many import GetDelegatesToGetMany
|
|
13
|
+
from sarj_python_lint.rules.fakes_in_shared_location import FakesInSharedLocation
|
|
14
|
+
from sarj_python_lint.rules.fastapi_explicit_openapi_contract import FastapiExplicitOpenapiContract
|
|
23
15
|
from sarj_python_lint.rules.iac_source_coupled_test import IacSourceCoupledTest
|
|
24
16
|
from sarj_python_lint.rules.invalid_pydantic_field_default import (
|
|
25
17
|
InvalidPydanticFieldDefault,
|
|
26
18
|
)
|
|
27
|
-
from sarj_python_lint.rules.kwarg_heavy_construction_in_test import KwargHeavyConstructionInTest
|
|
28
19
|
from sarj_python_lint.rules.mock_without_spec import MockWithoutSpec
|
|
29
20
|
from sarj_python_lint.rules.negative_only_http_status_assertion import (
|
|
30
21
|
NegativeOnlyHttpStatusAssertion,
|
|
31
22
|
)
|
|
32
|
-
from sarj_python_lint.rules.
|
|
33
|
-
|
|
23
|
+
from sarj_python_lint.rules.no_analytical_aggregation_in_postgres_store import (
|
|
24
|
+
NoAnalyticalAggregationInPostgresStore,
|
|
34
25
|
)
|
|
35
26
|
from sarj_python_lint.rules.no_comment_cruft import NoCommentCruft
|
|
36
27
|
from sarj_python_lint.rules.no_conftest_test_module_import import NoConftestTestModuleImport
|
|
28
|
+
from sarj_python_lint.rules.no_copied_inherited_docstring import NoCopiedInheritedDocstring
|
|
37
29
|
from sarj_python_lint.rules.no_cors_wildcard_with_credentials import (
|
|
38
30
|
NoCorsWildcardWithCredentials,
|
|
39
31
|
)
|
|
40
32
|
from sarj_python_lint.rules.no_duplicate_dunder_all_entry import NoDuplicateDunderAllEntry
|
|
41
33
|
from sarj_python_lint.rules.no_fastapi_on_event import NoFastapiOnEvent
|
|
42
|
-
from sarj_python_lint.rules.
|
|
34
|
+
from sarj_python_lint.rules.no_file_level_escape_hatch_suppression import (
|
|
35
|
+
NoFileLevelEscapeHatchSuppression,
|
|
36
|
+
)
|
|
43
37
|
from sarj_python_lint.rules.no_first_party_private_import import (
|
|
44
38
|
NoFirstPartyPrivateImport,
|
|
45
39
|
)
|
|
46
40
|
from sarj_python_lint.rules.no_frozen_after_validator_field_write import (
|
|
47
41
|
NoFrozenAfterValidatorFieldWrite,
|
|
48
42
|
)
|
|
49
|
-
from sarj_python_lint.rules.no_gen_random_uuid_in_sql import NoGenRandomUuidInSql
|
|
50
43
|
from sarj_python_lint.rules.no_generic_single_export_module import NoGenericSingleExportModule
|
|
51
44
|
from sarj_python_lint.rules.no_hidden_constructor_fallback import (
|
|
52
45
|
NoHiddenConstructorFallback,
|
|
@@ -59,8 +52,11 @@ from sarj_python_lint.rules.no_optional_tenant_predicate import (
|
|
|
59
52
|
NoOptionalTenantPredicate,
|
|
60
53
|
)
|
|
61
54
|
from sarj_python_lint.rules.no_query_with_many_joins import NoQueryWithManyJoins
|
|
55
|
+
from sarj_python_lint.rules.no_random_uuid_in_sql import NoRandomUuidInSql
|
|
56
|
+
from sarj_python_lint.rules.no_raw_connection_in_tests import NoRawConnectionInTests
|
|
62
57
|
from sarj_python_lint.rules.no_redundant_literal_description import NoRedundantLiteralDescription
|
|
63
58
|
from sarj_python_lint.rules.no_repeated_string_literal import NoRepeatedStringLiteral
|
|
59
|
+
from sarj_python_lint.rules.no_repeated_test_body import NoRepeatedTestBody
|
|
64
60
|
from sarj_python_lint.rules.no_restated_comment import NoRestatedComment
|
|
65
61
|
from sarj_python_lint.rules.no_secret_in_log import NoSecretInLog
|
|
66
62
|
from sarj_python_lint.rules.no_select_star import NoSelectStar
|
|
@@ -80,6 +76,7 @@ from sarj_python_lint.rules.opaque_parametrize_case_needs_id import OpaqueParame
|
|
|
80
76
|
from sarj_python_lint.rules.over_mocked_test import OverMockedTest
|
|
81
77
|
from sarj_python_lint.rules.phase_label_comment import TestPhaseLabelComment
|
|
82
78
|
from sarj_python_lint.rules.prefer_class_row import PreferClassRow
|
|
79
|
+
from sarj_python_lint.rules.prefer_collection_comprehension import PreferCollectionComprehension
|
|
83
80
|
from sarj_python_lint.rules.prefer_constant_time_secret_compare import (
|
|
84
81
|
PreferConstantTimeSecretCompare,
|
|
85
82
|
)
|
|
@@ -122,9 +119,11 @@ from sarj_python_lint.rules.preserve_declared_nominal_id import PreserveDeclared
|
|
|
122
119
|
from sarj_python_lint.rules.preserve_enum_types import PreserveEnumTypes
|
|
123
120
|
from sarj_python_lint.rules.production_derived_test_cases import ProductionDerivedTestCases
|
|
124
121
|
from sarj_python_lint.rules.pydantic_at_boundaries import PydanticAtBoundaries
|
|
122
|
+
from sarj_python_lint.rules.pytest_fixture_returns_bare_tuple import PytestFixtureReturnsBareTuple
|
|
125
123
|
from sarj_python_lint.rules.redundant_class_docstring import RedundantClassDocstring
|
|
126
124
|
from sarj_python_lint.rules.redundant_docstring import RedundantDocstring
|
|
127
125
|
from sarj_python_lint.rules.redundant_module_docstring import RedundantModuleDocstring
|
|
126
|
+
from sarj_python_lint.rules.repeated_kwarg_heavy_call_in_test import RepeatedKwargHeavyCallInTest
|
|
128
127
|
from sarj_python_lint.rules.repeated_static_call_cases import RepeatedStaticCallCases
|
|
129
128
|
from sarj_python_lint.rules.require_keyword_only_swap_prone_params import (
|
|
130
129
|
RequireKeywordOnlySwapProneParams,
|
|
@@ -144,9 +143,11 @@ from sarj_python_lint.rules.restated_test_docstring import RestatedTestDocstring
|
|
|
144
143
|
from sarj_python_lint.rules.source_coupled_test import SourceCoupledTest
|
|
145
144
|
from sarj_python_lint.rules.sql_requires_injected_pool_owner import SqlRequiresInjectedPoolOwner
|
|
146
145
|
from sarj_python_lint.rules.stepdown import Stepdown
|
|
146
|
+
from sarj_python_lint.rules.store_get_delegates_to_bulk_read import StoreGetDelegatesToBulkRead
|
|
147
147
|
from sarj_python_lint.rules.store_insert_requires_on_conflict import (
|
|
148
148
|
StoreInsertRequiresOnConflict,
|
|
149
149
|
)
|
|
150
|
+
from sarj_python_lint.rules.timestamp_order_requires_tiebreaker import TimestampOrderRequiresTiebreaker
|
|
150
151
|
from sarj_python_lint.rules.trailing_value_narration import TrailingValueNarration
|
|
151
152
|
from sarj_python_lint.rules.trivially_true_assertion import TriviallyTrueAssertion
|
|
152
153
|
from sarj_python_lint.rules.uncontrolled_randomness_in_test import UncontrolledRandomnessInTest
|
|
@@ -160,16 +161,18 @@ if TYPE_CHECKING:
|
|
|
160
161
|
|
|
161
162
|
REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
162
163
|
{
|
|
163
|
-
|
|
164
|
+
TimestampOrderRequiresTiebreaker.id: TimestampOrderRequiresTiebreaker,
|
|
164
165
|
NoStringConcatInLoop.id: NoStringConcatInLoop,
|
|
165
166
|
PreferClassRow.id: PreferClassRow,
|
|
167
|
+
PreferCollectionComprehension.id: PreferCollectionComprehension,
|
|
166
168
|
PreferStrEnum.id: PreferStrEnum,
|
|
167
169
|
NoIsinstanceUnionChain.id: NoIsinstanceUnionChain,
|
|
168
170
|
NoOffsetPagination.id: NoOffsetPagination,
|
|
169
171
|
PreferNamedtupleOverTupleReturn.id: PreferNamedtupleOverTupleReturn,
|
|
170
172
|
NoCorsWildcardWithCredentials.id: NoCorsWildcardWithCredentials,
|
|
171
173
|
PydanticAtBoundaries.id: PydanticAtBoundaries,
|
|
172
|
-
|
|
174
|
+
FastapiExplicitOpenapiContract.id: FastapiExplicitOpenapiContract,
|
|
175
|
+
FakesInSharedLocation.id: FakesInSharedLocation,
|
|
173
176
|
NoSentinelReturnOnExcept.id: NoSentinelReturnOnExcept,
|
|
174
177
|
PreferConstantTimeSecretCompare.id: PreferConstantTimeSecretCompare,
|
|
175
178
|
NoSecretInLog.id: NoSecretInLog,
|
|
@@ -181,7 +184,8 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
181
184
|
SourceCoupledTest.id: SourceCoupledTest,
|
|
182
185
|
IacSourceCoupledTest.id: IacSourceCoupledTest,
|
|
183
186
|
NoQueryWithManyJoins.id: NoQueryWithManyJoins,
|
|
184
|
-
|
|
187
|
+
NoRawConnectionInTests.id: NoRawConnectionInTests,
|
|
188
|
+
NoAnalyticalAggregationInPostgresStore.id: NoAnalyticalAggregationInPostgresStore,
|
|
185
189
|
NoSelectStar.id: NoSelectStar,
|
|
186
190
|
NoGenericSingleExportModule.id: NoGenericSingleExportModule,
|
|
187
191
|
Stepdown.id: Stepdown,
|
|
@@ -193,19 +197,19 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
193
197
|
PreferImmutableModuleConstant.id: PreferImmutableModuleConstant,
|
|
194
198
|
MockWithoutSpec.id: MockWithoutSpec,
|
|
195
199
|
OpaqueParametrizeCaseNeedsId.id: OpaqueParametrizeCaseNeedsId,
|
|
196
|
-
|
|
197
|
-
|
|
200
|
+
PytestFixtureReturnsBareTuple.id: PytestFixtureReturnsBareTuple,
|
|
201
|
+
StoreGetDelegatesToBulkRead.id: StoreGetDelegatesToBulkRead,
|
|
198
202
|
PreferOneForRequiredRow.id: PreferOneForRequiredRow,
|
|
199
|
-
|
|
200
|
-
|
|
203
|
+
RepeatedKwargHeavyCallInTest.id: RepeatedKwargHeavyCallInTest,
|
|
204
|
+
DefectXfailRequiresExplicitStrict.id: DefectXfailRequiresExplicitStrict,
|
|
201
205
|
NoFirstPartyPrivateImport.id: NoFirstPartyPrivateImport,
|
|
202
206
|
NoRestatedComment.id: NoRestatedComment,
|
|
203
207
|
RedundantDocstring.id: RedundantDocstring,
|
|
204
208
|
TrailingValueNarration.id: TrailingValueNarration,
|
|
205
209
|
NoStdlibLogging.id: NoStdlibLogging,
|
|
206
|
-
|
|
210
|
+
NoRandomUuidInSql.id: NoRandomUuidInSql,
|
|
207
211
|
NoHiddenConstructorFallback.id: NoHiddenConstructorFallback,
|
|
208
|
-
|
|
212
|
+
NoFileLevelEscapeHatchSuppression.id: NoFileLevelEscapeHatchSuppression,
|
|
209
213
|
NoFastapiOnEvent.id: NoFastapiOnEvent,
|
|
210
214
|
NoOptionalTenantPredicate.id: NoOptionalTenantPredicate,
|
|
211
215
|
NoTautologicalExpect.id: NoTautologicalExpect,
|
|
@@ -214,7 +218,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
214
218
|
InvalidPydanticFieldDefault.id: InvalidPydanticFieldDefault,
|
|
215
219
|
NoFrozenAfterValidatorFieldWrite.id: NoFrozenAfterValidatorFieldWrite,
|
|
216
220
|
TriviallyTrueAssertion.id: TriviallyTrueAssertion,
|
|
217
|
-
|
|
221
|
+
NoRepeatedTestBody.id: NoRepeatedTestBody,
|
|
218
222
|
UnusedMockSetup.id: UnusedMockSetup,
|
|
219
223
|
PreferFstringOverConcat.id: PreferFstringOverConcat,
|
|
220
224
|
PreferOrPattern.id: PreferOrPattern,
|
|
@@ -227,7 +231,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
227
231
|
PreferSelfTypeAnnotation.id: PreferSelfTypeAnnotation,
|
|
228
232
|
PreferSelfDocumentingConstant.id: PreferSelfDocumentingConstant,
|
|
229
233
|
NoDuplicateDunderAllEntry.id: NoDuplicateDunderAllEntry,
|
|
230
|
-
|
|
234
|
+
NoCopiedInheritedDocstring.id: NoCopiedInheritedDocstring,
|
|
231
235
|
RedundantClassDocstring.id: RedundantClassDocstring,
|
|
232
236
|
RedundantModuleDocstring.id: RedundantModuleDocstring,
|
|
233
237
|
DocstringArgsRestateSignature.id: DocstringArgsRestateSignature,
|
|
@@ -38,7 +38,7 @@ _GITHUB_ISSUE_RE = re.compile(r"https?://github\.com/[^/\s]+/[^/\s]+/issues/\d+\
|
|
|
38
38
|
_NONDETERMINISM_RE = re.compile(r"intermittent|flak|sometimes|non-?deterministic|varies", re.IGNORECASE)
|
|
39
39
|
|
|
40
40
|
# Sibling markers that declare a nondeterministic dependency.
|
|
41
|
-
_NONDETERMINISTIC_MARKERS = frozenset({"real_llm", "flaky", "network"
|
|
41
|
+
_NONDETERMINISTIC_MARKERS = frozenset({"real_llm", "flaky", "network"})
|
|
42
42
|
|
|
43
43
|
# Hypothesis' entry point.
|
|
44
44
|
_PROPERTY_DECORATORS = frozenset({"given"})
|
|
@@ -53,19 +53,23 @@ _FUNC_NODES = (ast.FunctionDef, ast.AsyncFunctionDef)
|
|
|
53
53
|
_PYTESTMARK = "pytestmark"
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
class
|
|
57
|
-
id: str = "defect-xfail-requires-strict"
|
|
56
|
+
class DefectXfailRequiresExplicitStrict(Rule):
|
|
57
|
+
id: str = "defect-xfail-requires-explicit-strict"
|
|
58
58
|
code: str = "SARJ046"
|
|
59
59
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
60
|
-
summary="
|
|
61
|
-
rationale=
|
|
60
|
+
summary="Defect-pinning `xfail` must explicitly set `strict=True` so an XPASS cannot silently pass.",
|
|
61
|
+
rationale=(
|
|
62
|
+
"A defect pin that relies on pytest's configurable strictness default can stay green after the defect is "
|
|
63
|
+
"fixed, leaving stale coverage and markers behind. Local strictness keeps the invariant attached to the pin."
|
|
64
|
+
),
|
|
62
65
|
remediation="Set `strict=True` so an unexpected pass fails and prompts removal of the obsolete marker.",
|
|
63
66
|
category=RuleCategory.TESTING,
|
|
64
|
-
aliases=("xfail-requires-strict",),
|
|
67
|
+
aliases=("defect-xfail-requires-strict", "xfail-requires-strict"),
|
|
65
68
|
limitations=(
|
|
66
69
|
"Only markers resolved through an unambiguous pytest or pytest.mark import are analyzed.",
|
|
67
70
|
"Only xfail reasons that explicitly identify a defect are analyzed.",
|
|
68
|
-
"Nondeterministic, property-based,
|
|
71
|
+
"Nondeterministic, property-based, network, environment-gated, run=False, and statically disabled pins are excluded.",
|
|
72
|
+
"Repository-level strict_xfail / xfail_strict configuration is intentionally not consulted: defect pins remain locally explicit.",
|
|
69
73
|
),
|
|
70
74
|
examples=(
|
|
71
75
|
RuleExample(
|
|
@@ -134,6 +138,7 @@ def _rotting_bug_pins(tree: ast.Module, imports: ImportIndex) -> list[ast.Call]:
|
|
|
134
138
|
hits.extend(_rotting_markers(node.decorator_list, imports))
|
|
135
139
|
for node in nodes(tree, *_FUNC_NODES):
|
|
136
140
|
hits.extend(_rotting_markers(node.decorator_list, imports))
|
|
141
|
+
hits.extend(_rotting_param_markers(node.decorator_list, imports))
|
|
137
142
|
return hits
|
|
138
143
|
|
|
139
144
|
|
|
@@ -143,6 +148,18 @@ def _rotting_markers(markers: list[ast.expr], imports: ImportIndex) -> list[ast.
|
|
|
143
148
|
return [marker for marker in markers if isinstance(marker, ast.Call) and _is_rotting_xfail(marker, imports)]
|
|
144
149
|
|
|
145
150
|
|
|
151
|
+
def _rotting_param_markers(decorators: list[ast.expr], imports: ImportIndex) -> list[ast.Call]:
|
|
152
|
+
if _has_nondeterministic_marker(decorators, imports):
|
|
153
|
+
return []
|
|
154
|
+
return [
|
|
155
|
+
nested
|
|
156
|
+
for decorator in decorators
|
|
157
|
+
if _pytest_marker_name(decorator, imports) == _PARAMETRIZE_ATTR
|
|
158
|
+
for nested in walk(decorator)
|
|
159
|
+
if isinstance(nested, ast.Call) and nested is not decorator and _is_rotting_xfail(nested, imports)
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
|
|
146
163
|
def _module_pytest_markers(tree: ast.Module, imports: ImportIndex) -> list[ast.expr]:
|
|
147
164
|
values: list[ast.expr] = []
|
|
148
165
|
for statement in tree.body:
|
|
@@ -191,7 +208,7 @@ def _pytest_marker_name(dec: ast.expr, imports: ImportIndex) -> str | None:
|
|
|
191
208
|
def _is_rotting_xfail(dec: ast.expr, imports: ImportIndex) -> bool:
|
|
192
209
|
if not isinstance(dec, ast.Call) or _pytest_marker_name(dec, imports) != _XFAIL:
|
|
193
210
|
return False
|
|
194
|
-
if _is_strict(dec) or _is_environment_gated(dec):
|
|
211
|
+
if _is_strict(dec) or _cannot_xpass(dec) or _is_environment_gated(dec):
|
|
195
212
|
return False
|
|
196
213
|
reason = _reason_text(dec)
|
|
197
214
|
if reason is None or _NONDETERMINISM_RE.search(reason):
|
|
@@ -235,6 +252,15 @@ def _is_strict(dec: ast.Call) -> bool:
|
|
|
235
252
|
return False
|
|
236
253
|
|
|
237
254
|
|
|
255
|
+
def _cannot_xpass(dec: ast.Call) -> bool:
|
|
256
|
+
for kw in dec.keywords:
|
|
257
|
+
if kw.arg == "run" and isinstance(kw.value, ast.Constant) and kw.value.value is False:
|
|
258
|
+
return True
|
|
259
|
+
if kw.arg == "condition" and isinstance(kw.value, ast.Constant) and kw.value.value is False:
|
|
260
|
+
return True
|
|
261
|
+
return bool(dec.args and isinstance(dec.args[0], ast.Constant) and dec.args[0].value is False)
|
|
262
|
+
|
|
263
|
+
|
|
238
264
|
def _reason_text(dec: ast.Call) -> str | None:
|
|
239
265
|
for kw in dec.keywords:
|
|
240
266
|
if kw.arg == "reason":
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
import re
|
|
6
|
+
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
7
|
+
|
|
8
|
+
from sarj_python_lint.rule_base import (
|
|
9
|
+
AutofixPolicy,
|
|
10
|
+
Diagnostic,
|
|
11
|
+
ExampleFile,
|
|
12
|
+
ExampleOutcome,
|
|
13
|
+
Rule,
|
|
14
|
+
RuleCategory,
|
|
15
|
+
RuleDocumentation,
|
|
16
|
+
RuleExample,
|
|
17
|
+
parse_or_none,
|
|
18
|
+
)
|
|
19
|
+
from sarj_python_lint.rules._paths import is_generated, is_test_path, is_test_support_path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_DOUBLE_NAME_RE = re.compile(r"^(?:Fake|Stub|Mock|InMemory|Recording|Scripted)[A-Z0-9]")
|
|
27
|
+
_SHARED_SUPPORT_NAMES = frozenset({"helpers", "support", "test_support", "test_utils"})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@final
|
|
31
|
+
class FakesInSharedLocation(Rule):
|
|
32
|
+
id = "fakes-in-shared-location"
|
|
33
|
+
code = "SARJ428"
|
|
34
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
35
|
+
summary="Define reusable test doubles in a shared testing-support module.",
|
|
36
|
+
rationale=(
|
|
37
|
+
"A fake embedded in one test module silently becomes a private second implementation of a contract. "
|
|
38
|
+
"A named support module makes the double discoverable, reusable, and reviewable as test infrastructure."
|
|
39
|
+
),
|
|
40
|
+
remediation=(
|
|
41
|
+
"Move the class to a testing, fakes, stubs, mocks, doubles, or test_utils module and import it. "
|
|
42
|
+
"Use an exact SARJ428 suppression only when the double is intentionally coupled to one scenario."
|
|
43
|
+
),
|
|
44
|
+
category=RuleCategory.TESTING,
|
|
45
|
+
autofix=AutofixPolicy.NONE,
|
|
46
|
+
limitations=(
|
|
47
|
+
"Only top-level classes in test files with an explicit Fake, Stub, Mock, InMemory, Recording, or Scripted prefix are classified as doubles.",
|
|
48
|
+
"conftest.py and conventional shared-support paths are already shared locations and are excluded.",
|
|
49
|
+
),
|
|
50
|
+
examples=(
|
|
51
|
+
RuleExample(
|
|
52
|
+
example_id="local-fake-service",
|
|
53
|
+
title="A test module privately defines a named fake",
|
|
54
|
+
outcome=ExampleOutcome.MATCH,
|
|
55
|
+
files=(
|
|
56
|
+
ExampleFile.python(
|
|
57
|
+
"tests/test_checkout.py",
|
|
58
|
+
"class FakePaymentGateway:\n async def charge(self, amount: int) -> None:\n pass\n",
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
focus_path=PurePosixPath("tests/test_checkout.py"),
|
|
62
|
+
expected_count=1,
|
|
63
|
+
public=True,
|
|
64
|
+
),
|
|
65
|
+
RuleExample(
|
|
66
|
+
example_id="shared-fake-service",
|
|
67
|
+
title="A shared support module owns the fake",
|
|
68
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
69
|
+
files=(
|
|
70
|
+
ExampleFile.python(
|
|
71
|
+
"tests/fakes/payment.py",
|
|
72
|
+
"class FakePaymentGateway:\n async def charge(self, amount: int) -> None:\n pass\n",
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
focus_path=PurePosixPath("tests/fakes/payment.py"),
|
|
76
|
+
expected_count=0,
|
|
77
|
+
public=True,
|
|
78
|
+
),
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
description = documentation.summary
|
|
82
|
+
|
|
83
|
+
@override
|
|
84
|
+
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
85
|
+
if not is_test_path(path) or _is_shared_support(path) or is_generated(path, source):
|
|
86
|
+
return []
|
|
87
|
+
tree = parse_or_none(path, source)
|
|
88
|
+
if tree is None:
|
|
89
|
+
return []
|
|
90
|
+
return [
|
|
91
|
+
Diagnostic(
|
|
92
|
+
path=path,
|
|
93
|
+
line=node.lineno,
|
|
94
|
+
col=node.col_offset + 1,
|
|
95
|
+
code=self.code,
|
|
96
|
+
message=(
|
|
97
|
+
f"test double `{node.name}` is private to this test module; move it to shared testing support "
|
|
98
|
+
"or add an exact SARJ428 suppression for a deliberately scenario-local double"
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
for node in tree.body
|
|
102
|
+
if isinstance(node, ast.ClassDef) and _DOUBLE_NAME_RE.match(node.name)
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _is_shared_support(path: Path) -> bool:
|
|
107
|
+
names = {part.lower() for part in path.parts}
|
|
108
|
+
return (
|
|
109
|
+
path.name == "conftest.py"
|
|
110
|
+
or path.stem.lower() in _SHARED_SUPPORT_NAMES
|
|
111
|
+
or bool(names & _SHARED_SUPPORT_NAMES)
|
|
112
|
+
or is_test_support_path(path)
|
|
113
|
+
)
|
|
@@ -44,6 +44,8 @@ _CONTAINERS = frozenset({"list", "List", "set", "Set", "tuple", "Tuple", "Sequen
|
|
|
44
44
|
_BODY_MARKERS = frozenset({"Body", "Form", "File"})
|
|
45
45
|
_NO_CONTENT_STATUSES = frozenset({204, 304})
|
|
46
46
|
_STATUS_CODE_DIGITS = 3
|
|
47
|
+
_MIN_HTTP_STATUS = 100
|
|
48
|
+
_MAX_HTTP_STATUS = 599
|
|
47
49
|
_DOCUMENTATION_EXAMPLE_DIR_NAMES = frozenset({"docs_src"})
|
|
48
50
|
_CONVERTER_PATTERNS = MappingProxyType(
|
|
49
51
|
{
|
|
@@ -62,14 +64,18 @@ class _Finding:
|
|
|
62
64
|
message: str
|
|
63
65
|
|
|
64
66
|
|
|
65
|
-
class
|
|
66
|
-
id: str = "fastapi-openapi-contract"
|
|
67
|
+
class FastapiExplicitOpenapiContract(Rule):
|
|
68
|
+
id: str = "fastapi-explicit-openapi-contract"
|
|
67
69
|
code: str = "SARJ094"
|
|
68
70
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
69
|
-
summary="FastAPI operations must
|
|
70
|
-
rationale=
|
|
71
|
+
summary="FastAPI operations must make request, response, and status contracts explicit in generated OpenAPI.",
|
|
72
|
+
rationale=(
|
|
73
|
+
"Locally explicit route metadata keeps generated OpenAPI reviewable and stable across framework defaults "
|
|
74
|
+
"without duplicating self-documenting names in prose."
|
|
75
|
+
),
|
|
71
76
|
remediation="Declare status codes, typed parameters, response schemas, and alternate responses raised directly by the handler.",
|
|
72
77
|
category=RuleCategory.CORRECTNESS,
|
|
78
|
+
aliases=("fastapi-openapi-contract",),
|
|
73
79
|
limitations=(
|
|
74
80
|
"Hidden routes, WebSocket handlers, tests, generated files, documentation-source examples, and unrelated decorators are excluded.",
|
|
75
81
|
"Dynamic response mappings are accepted when their contents cannot be resolved statically.",
|
|
@@ -134,8 +140,9 @@ class FastapiOpenapiContract(Rule):
|
|
|
134
140
|
findings.extend(_check_raw_request(function, routes, index))
|
|
135
141
|
findings.extend(_check_direct_responses(function, routes, index))
|
|
136
142
|
for route in _operations(routes):
|
|
137
|
-
findings.extend(_check_metadata(route))
|
|
143
|
+
findings.extend(_check_metadata(route, index))
|
|
138
144
|
findings.extend(_check_projection(route))
|
|
145
|
+
findings.extend(_check_response_statuses(route, index))
|
|
139
146
|
findings.extend(_check_route_conflicts(declared))
|
|
140
147
|
return [
|
|
141
148
|
Diagnostic(
|
|
@@ -155,11 +162,15 @@ def _operations(routes: tuple[Route, ...]) -> tuple[Route, ...]:
|
|
|
155
162
|
return tuple({id(route.decorator): route for route in routes}.values())
|
|
156
163
|
|
|
157
164
|
|
|
158
|
-
def _check_metadata(route: Route) -> list[_Finding]:
|
|
159
|
-
if route.has_unpack:
|
|
160
|
-
return []
|
|
165
|
+
def _check_metadata(route: Route, index: FastapiIndex) -> list[_Finding]:
|
|
161
166
|
keywords = route.keywords
|
|
162
|
-
|
|
167
|
+
status_node = keywords.get("status_code")
|
|
168
|
+
if status_node is not None and not _literal_none(status_node):
|
|
169
|
+
status = _status_code(status_node, index)
|
|
170
|
+
if status is not None and not _valid_http_status(status):
|
|
171
|
+
return [_Finding(status_node, f"[status] {status} is outside the HTTP status range 100..599.")]
|
|
172
|
+
return []
|
|
173
|
+
if route.has_unpack:
|
|
163
174
|
return []
|
|
164
175
|
return [_Finding(route.decorator, "[metadata] operation requires explicit status_code.")]
|
|
165
176
|
|
|
@@ -454,6 +465,7 @@ def _check_direct_responses(
|
|
|
454
465
|
index: FastapiIndex,
|
|
455
466
|
) -> list[_Finding]:
|
|
456
467
|
statuses: set[int] = set()
|
|
468
|
+
invalid: dict[int, ast.Call] = {}
|
|
457
469
|
stack: list[ast.AST] = list(function.body)
|
|
458
470
|
while stack:
|
|
459
471
|
current = stack.pop()
|
|
@@ -465,16 +477,25 @@ def _check_direct_responses(
|
|
|
465
477
|
if isinstance(call, ast.Call) and index.is_http_exception(call.func):
|
|
466
478
|
status = _status_code(_keyword(call, "status_code") or (call.args[0] if call.args else None), index)
|
|
467
479
|
if status is not None:
|
|
468
|
-
|
|
480
|
+
if _valid_http_status(status):
|
|
481
|
+
statuses.add(status)
|
|
482
|
+
else:
|
|
483
|
+
invalid.setdefault(status, call)
|
|
469
484
|
returned = current.value if isinstance(current, ast.Return) else None
|
|
470
485
|
if isinstance(returned, ast.Call) and index.is_response(returned.func):
|
|
471
486
|
status = _status_code(_keyword(returned, "status_code"), index)
|
|
472
487
|
if status is not None:
|
|
473
|
-
|
|
488
|
+
if _valid_http_status(status):
|
|
489
|
+
statuses.add(status)
|
|
490
|
+
else:
|
|
491
|
+
invalid.setdefault(status, returned)
|
|
474
492
|
stack.extend(children(current))
|
|
493
|
+
findings = [
|
|
494
|
+
_Finding(node, f"[status] {status} is outside the HTTP status range 100..599.")
|
|
495
|
+
for status, node in invalid.items()
|
|
496
|
+
]
|
|
475
497
|
if not statuses:
|
|
476
|
-
return
|
|
477
|
-
findings: list[_Finding] = []
|
|
498
|
+
return findings
|
|
478
499
|
for route in _operations(routes):
|
|
479
500
|
responses = route.keywords.get("responses")
|
|
480
501
|
if responses is not None and not isinstance(responses, ast.Dict):
|
|
@@ -492,6 +513,22 @@ def _check_direct_responses(
|
|
|
492
513
|
return findings
|
|
493
514
|
|
|
494
515
|
|
|
516
|
+
def _check_response_statuses(route: Route, index: FastapiIndex) -> list[_Finding]:
|
|
517
|
+
responses = route.keywords.get("responses")
|
|
518
|
+
if not isinstance(responses, ast.Dict):
|
|
519
|
+
return []
|
|
520
|
+
findings: list[_Finding] = []
|
|
521
|
+
for key in responses.keys:
|
|
522
|
+
status = _status_code(key, index)
|
|
523
|
+
if key is not None and status is not None and not _valid_http_status(status):
|
|
524
|
+
findings.append(_Finding(key, f"[status] {status} is outside the HTTP status range 100..599."))
|
|
525
|
+
return findings
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _valid_http_status(status: int) -> bool:
|
|
529
|
+
return _MIN_HTTP_STATUS <= status <= _MAX_HTTP_STATUS
|
|
530
|
+
|
|
531
|
+
|
|
495
532
|
def _status_code(node: ast.expr | None, index: FastapiIndex) -> int | None:
|
|
496
533
|
if isinstance(node, ast.Constant) and isinstance(node.value, int):
|
|
497
534
|
return node.value
|
|
@@ -33,7 +33,8 @@ class IacSourceCoupledTest(Rule):
|
|
|
33
33
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
34
34
|
summary="Test asserts on raw IaC source text instead of a parsed plan, provider state, or runtime behavior.",
|
|
35
35
|
rationale=(
|
|
36
|
-
"Substring and regex checks can pass on comments, formatting, or unreachable Terraform configuration
|
|
36
|
+
"Substring and regex checks can pass on comments, formatting, or unreachable Terraform configuration "
|
|
37
|
+
"without proving the plan or deployed behavior."
|
|
37
38
|
),
|
|
38
39
|
remediation="Parse rendered plan JSON, query the provider, or exercise the deployed runtime contract.",
|
|
39
40
|
category=RuleCategory.TESTING,
|
|
@@ -45,7 +46,7 @@ class IacSourceCoupledTest(Rule):
|
|
|
45
46
|
examples=(
|
|
46
47
|
RuleExample(
|
|
47
48
|
example_id="rendered-plan-contract",
|
|
48
|
-
title="Assert on
|
|
49
|
+
title="Assert on structured Terraform plan behavior",
|
|
49
50
|
outcome=ExampleOutcome.NO_MATCH,
|
|
50
51
|
files=(
|
|
51
52
|
ExampleFile.python(
|