sarj-python-lint 0.73.2__tar.gz → 0.75.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/PKG-INFO +1 -1
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/pyproject.toml +1 -1
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_registry.py +4 -2
- sarj_python_lint-0.75.0/src/sarj_python_lint/rules/get_delegates_to_get_many.py +282 -0
- sarj_python_lint-0.75.0/src/sarj_python_lint/rules/prefer_one_for_required_row.py +175 -0
- sarj_python_lint-0.73.2/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -121
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/.gitignore +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/LICENSE +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/README.md +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/created_at_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/defect_xfail_requires_strict.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/duplicate_test_body.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/duplicated_override_docstring.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/fastapi_openapi_contract.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_long_comment.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/preserve_declared_nominal_id.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/preserve_enum_types.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/require_validated_row_factory.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/source_coupled_test.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/sql_requires_injected_pool_owner.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/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.
|
|
3
|
+
Version: 0.75.0
|
|
4
4
|
Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
|
|
5
5
|
Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
|
|
6
6
|
Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
|
|
@@ -19,6 +19,7 @@ from sarj_python_lint.rules.duplicated_override_docstring import (
|
|
|
19
19
|
)
|
|
20
20
|
from sarj_python_lint.rules.fastapi_openapi_contract import FastapiOpenapiContract
|
|
21
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
|
|
22
23
|
from sarj_python_lint.rules.iac_source_coupled_test import IacSourceCoupledTest
|
|
23
24
|
from sarj_python_lint.rules.invalid_pydantic_field_default import (
|
|
24
25
|
InvalidPydanticFieldDefault,
|
|
@@ -37,7 +38,6 @@ from sarj_python_lint.rules.no_cors_wildcard_with_credentials import (
|
|
|
37
38
|
)
|
|
38
39
|
from sarj_python_lint.rules.no_duplicate_dunder_all_entry import NoDuplicateDunderAllEntry
|
|
39
40
|
from sarj_python_lint.rules.no_file_level_escape_hatch_noqa import NoFileLevelEscapeHatchNoqa
|
|
40
|
-
from sarj_python_lint.rules.no_file_level_suppression import NoFileLevelSuppression
|
|
41
41
|
from sarj_python_lint.rules.no_first_party_private_import import (
|
|
42
42
|
NoFirstPartyPrivateImport,
|
|
43
43
|
)
|
|
@@ -96,6 +96,7 @@ from sarj_python_lint.rules.prefer_nominal_id_types import PreferNominalIdTypes
|
|
|
96
96
|
from sarj_python_lint.rules.prefer_non_nullable_collection import (
|
|
97
97
|
PreferNonNullableCollection,
|
|
98
98
|
)
|
|
99
|
+
from sarj_python_lint.rules.prefer_one_for_required_row import PreferOneForRequiredRow
|
|
99
100
|
from sarj_python_lint.rules.prefer_or_pattern import PreferOrPattern
|
|
100
101
|
from sarj_python_lint.rules.prefer_self_documenting_constant import (
|
|
101
102
|
PreferSelfDocumentingConstant,
|
|
@@ -180,12 +181,13 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
180
181
|
PreferMatchAssertNever.id: PreferMatchAssertNever,
|
|
181
182
|
RequireKeywordOnlySwapProneParams.id: RequireKeywordOnlySwapProneParams,
|
|
182
183
|
RequirePydanticForExternalJson.id: RequirePydanticForExternalJson,
|
|
183
|
-
NoFileLevelSuppression.id: NoFileLevelSuppression,
|
|
184
184
|
PreferModuleLevelConstant.id: PreferModuleLevelConstant,
|
|
185
185
|
PreferImmutableModuleConstant.id: PreferImmutableModuleConstant,
|
|
186
186
|
MockWithoutSpec.id: MockWithoutSpec,
|
|
187
187
|
OpaqueParametrizeCaseNeedsId.id: OpaqueParametrizeCaseNeedsId,
|
|
188
188
|
FixtureReturnsBareTuple.id: FixtureReturnsBareTuple,
|
|
189
|
+
GetDelegatesToGetMany.id: GetDelegatesToGetMany,
|
|
190
|
+
PreferOneForRequiredRow.id: PreferOneForRequiredRow,
|
|
189
191
|
KwargHeavyConstructionInTest.id: KwargHeavyConstructionInTest,
|
|
190
192
|
DefectXfailRequiresStrict.id: DefectXfailRequiresStrict,
|
|
191
193
|
NoFirstPartyPrivateImport.id: NoFirstPartyPrivateImport,
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar, NamedTuple, 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
|
+
parse_or_none,
|
|
18
|
+
)
|
|
19
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
20
|
+
from sarj_python_lint.rules._sql import is_store_module
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
_BULK_NAMES = frozenset({"get_by_ids", "get_many"})
|
|
28
|
+
_BRANCH_NODES = (ast.If, ast.IfExp, ast.Match, ast.Try, ast.TryStar)
|
|
29
|
+
_PAIR_SIZE = 2
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _MethodPair(NamedTuple):
|
|
33
|
+
singleton: ast.FunctionDef | ast.AsyncFunctionDef
|
|
34
|
+
bulk: ast.FunctionDef | ast.AsyncFunctionDef
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _BulkResultTypes(NamedTuple):
|
|
38
|
+
key: ast.expr | None
|
|
39
|
+
value: ast.expr
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@final
|
|
43
|
+
class GetDelegatesToGetMany(Rule):
|
|
44
|
+
id = "get-delegates-to-get-many"
|
|
45
|
+
code = "SARJ421"
|
|
46
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
47
|
+
summary="Require compatible singleton store reads to delegate to their bulk implementation.",
|
|
48
|
+
rationale=(
|
|
49
|
+
"Independent singleton and bulk queries can drift in filtering, row conversion, authorization, "
|
|
50
|
+
"and missing-row behavior while maintaining two database access paths."
|
|
51
|
+
),
|
|
52
|
+
remediation=(
|
|
53
|
+
"Implement `get` through the compatible `get_many([key])` or `get_by_ids([key])` method and "
|
|
54
|
+
"project its documented zero-or-one result."
|
|
55
|
+
),
|
|
56
|
+
category=RuleCategory.MAINTAINABILITY,
|
|
57
|
+
autofix=AutofixPolicy.NONE,
|
|
58
|
+
limitations=(
|
|
59
|
+
"Only concrete methods declared together in a production store module are inspected.",
|
|
60
|
+
"The methods must have one typed key, a list result for get_many or dict result for get_by_ids, and the same sync shape.",
|
|
61
|
+
"Branching singleton implementations are excluded because caching, locking, validation, or authorization may differ intentionally.",
|
|
62
|
+
),
|
|
63
|
+
examples=(
|
|
64
|
+
RuleExample(
|
|
65
|
+
example_id="duplicate-singleton-query",
|
|
66
|
+
title="Do not maintain a second singleton query path",
|
|
67
|
+
outcome=ExampleOutcome.MATCH,
|
|
68
|
+
files=(
|
|
69
|
+
ExampleFile.python(
|
|
70
|
+
"app/user_store.py",
|
|
71
|
+
"class UserStore:\n"
|
|
72
|
+
" async def get(self, user_id: UserId) -> User | None:\n"
|
|
73
|
+
" return await self.query_one(user_id)\n\n"
|
|
74
|
+
" async def get_many(self, user_ids: list[UserId]) -> list[User]:\n"
|
|
75
|
+
" return await self.query_many(user_ids)\n",
|
|
76
|
+
),
|
|
77
|
+
),
|
|
78
|
+
focus_path=PurePosixPath("app/user_store.py"),
|
|
79
|
+
expected_count=1,
|
|
80
|
+
public=True,
|
|
81
|
+
),
|
|
82
|
+
RuleExample(
|
|
83
|
+
example_id="singleton-delegates",
|
|
84
|
+
title="Delegate the singleton read to the bulk implementation",
|
|
85
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
86
|
+
files=(
|
|
87
|
+
ExampleFile.python(
|
|
88
|
+
"app/user_store.py",
|
|
89
|
+
"class UserStore:\n"
|
|
90
|
+
" async def get(self, user_id: UserId) -> User | None:\n"
|
|
91
|
+
" rows = await self.get_many([user_id])\n"
|
|
92
|
+
" return rows[0] if rows else None\n\n"
|
|
93
|
+
" async def get_many(self, user_ids: list[UserId]) -> list[User]:\n"
|
|
94
|
+
" return await self.query_many(user_ids)\n",
|
|
95
|
+
),
|
|
96
|
+
),
|
|
97
|
+
focus_path=PurePosixPath("app/user_store.py"),
|
|
98
|
+
expected_count=0,
|
|
99
|
+
public=True,
|
|
100
|
+
),
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
description = documentation.summary
|
|
104
|
+
|
|
105
|
+
@override
|
|
106
|
+
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
107
|
+
if not is_store_module(path) or is_generated(path, source):
|
|
108
|
+
return []
|
|
109
|
+
tree = parse_or_none(path, source)
|
|
110
|
+
if tree is None:
|
|
111
|
+
return []
|
|
112
|
+
diagnostics: list[Diagnostic] = []
|
|
113
|
+
for statement in tree.body:
|
|
114
|
+
if not isinstance(statement, ast.ClassDef):
|
|
115
|
+
continue
|
|
116
|
+
pair = _compatible_pair(statement)
|
|
117
|
+
if pair is None:
|
|
118
|
+
continue
|
|
119
|
+
singleton, bulk = pair
|
|
120
|
+
if _has_branch(singleton) or _calls_method(singleton, bulk.name) or _calls_method(bulk, "get"):
|
|
121
|
+
continue
|
|
122
|
+
diagnostics.append(
|
|
123
|
+
Diagnostic(
|
|
124
|
+
path=path,
|
|
125
|
+
line=singleton.lineno,
|
|
126
|
+
col=singleton.col_offset + 1,
|
|
127
|
+
code=self.code,
|
|
128
|
+
message=(
|
|
129
|
+
f"This store defines compatible `get` and `{bulk.name}` methods; implement `get` through "
|
|
130
|
+
f"`{bulk.name}([key])` so singleton and bulk reads share one contract, or document why "
|
|
131
|
+
"their semantics differ."
|
|
132
|
+
),
|
|
133
|
+
severity=Severity.WARNING,
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
return diagnostics
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _compatible_pair(
|
|
140
|
+
owner: ast.ClassDef,
|
|
141
|
+
) -> _MethodPair | None:
|
|
142
|
+
methods = [node for node in owner.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))]
|
|
143
|
+
singletons = [node for node in methods if node.name == "get" and _is_concrete(node)]
|
|
144
|
+
bulks = [node for node in methods if node.name in _BULK_NAMES and _is_concrete(node)]
|
|
145
|
+
if len(singletons) != 1 or len(bulks) != 1:
|
|
146
|
+
return None
|
|
147
|
+
singleton, bulk = singletons[0], bulks[0]
|
|
148
|
+
if isinstance(singleton, ast.AsyncFunctionDef) is not isinstance(bulk, ast.AsyncFunctionDef):
|
|
149
|
+
return None
|
|
150
|
+
singleton_key = _single_key_annotation(singleton)
|
|
151
|
+
bulk_key = _bulk_key_annotation(bulk)
|
|
152
|
+
singleton_value = _nullable_value(singleton.returns)
|
|
153
|
+
bulk_types = _bulk_result_types(bulk.returns, bulk.name)
|
|
154
|
+
if singleton_key is None or bulk_key is None or singleton_value is None or bulk_types is None:
|
|
155
|
+
return None
|
|
156
|
+
result_key, result_value = bulk_types
|
|
157
|
+
if ast.dump(singleton_key, include_attributes=False) != ast.dump(bulk_key, include_attributes=False):
|
|
158
|
+
return None
|
|
159
|
+
if result_key is not None and ast.dump(singleton_key, include_attributes=False) != ast.dump(
|
|
160
|
+
result_key, include_attributes=False
|
|
161
|
+
):
|
|
162
|
+
return None
|
|
163
|
+
if ast.dump(singleton_value, include_attributes=False) != ast.dump(result_value, include_attributes=False):
|
|
164
|
+
return None
|
|
165
|
+
return _MethodPair(singleton, bulk)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _is_concrete(method: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
169
|
+
if any(
|
|
170
|
+
_qualified_name(decorator).split(".")[-1] in {"abstractmethod", "overload"}
|
|
171
|
+
for decorator in method.decorator_list
|
|
172
|
+
):
|
|
173
|
+
return False
|
|
174
|
+
return not (
|
|
175
|
+
len(method.body) == 1
|
|
176
|
+
and (
|
|
177
|
+
isinstance(method.body[0], ast.Pass)
|
|
178
|
+
or (
|
|
179
|
+
isinstance(method.body[0], ast.Expr)
|
|
180
|
+
and isinstance(method.body[0].value, ast.Constant)
|
|
181
|
+
and method.body[0].value.value is Ellipsis
|
|
182
|
+
)
|
|
183
|
+
or isinstance(method.body[0], ast.Raise)
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _single_key_annotation(method: ast.FunctionDef | ast.AsyncFunctionDef) -> ast.expr | None:
|
|
189
|
+
if method.args.posonlyargs or method.args.vararg or method.args.kwarg or method.args.kwonlyargs:
|
|
190
|
+
return None
|
|
191
|
+
if len(method.args.args) != _PAIR_SIZE or method.args.defaults:
|
|
192
|
+
return None
|
|
193
|
+
self_arg, key_arg = method.args.args
|
|
194
|
+
if self_arg.arg not in {"self", "cls"}:
|
|
195
|
+
return None
|
|
196
|
+
return key_arg.annotation
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _bulk_key_annotation(method: ast.FunctionDef | ast.AsyncFunctionDef) -> ast.expr | None:
|
|
200
|
+
annotation = _single_key_annotation(method)
|
|
201
|
+
if isinstance(annotation, ast.Subscript) and _qualified_name(annotation.value) in {
|
|
202
|
+
"List",
|
|
203
|
+
"Sequence",
|
|
204
|
+
"list",
|
|
205
|
+
"typing.List",
|
|
206
|
+
"typing.Sequence",
|
|
207
|
+
}:
|
|
208
|
+
return annotation.slice
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _nullable_value(annotation: ast.expr | None) -> ast.expr | None:
|
|
213
|
+
if isinstance(annotation, ast.Subscript) and _qualified_name(annotation.value) in {"Optional", "typing.Optional"}:
|
|
214
|
+
return annotation.slice
|
|
215
|
+
if isinstance(annotation, ast.BinOp) and isinstance(annotation.op, ast.BitOr):
|
|
216
|
+
if _is_none_annotation(annotation.left):
|
|
217
|
+
return annotation.right
|
|
218
|
+
if _is_none_annotation(annotation.right):
|
|
219
|
+
return annotation.left
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _bulk_result_types(annotation: ast.expr | None, name: str) -> _BulkResultTypes | None:
|
|
224
|
+
if not isinstance(annotation, ast.Subscript):
|
|
225
|
+
return None
|
|
226
|
+
base = _qualified_name(annotation.value)
|
|
227
|
+
if name == "get_many" and base in {"List", "list", "typing.List"}:
|
|
228
|
+
return _BulkResultTypes(None, annotation.slice)
|
|
229
|
+
if (
|
|
230
|
+
name == "get_by_ids"
|
|
231
|
+
and base in {"Dict", "dict", "typing.Dict"}
|
|
232
|
+
and isinstance(annotation.slice, ast.Tuple)
|
|
233
|
+
and len(annotation.slice.elts) == _PAIR_SIZE
|
|
234
|
+
):
|
|
235
|
+
return _BulkResultTypes(annotation.slice.elts[0], annotation.slice.elts[1])
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _has_branch(method: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
240
|
+
return any(isinstance(node, _BRANCH_NODES) for node in _method_nodes(method))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _calls_method(
|
|
244
|
+
method: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
245
|
+
called_name: str,
|
|
246
|
+
) -> bool:
|
|
247
|
+
for node in _method_nodes(method):
|
|
248
|
+
if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute):
|
|
249
|
+
continue
|
|
250
|
+
if (
|
|
251
|
+
not isinstance(node.func.value, ast.Name)
|
|
252
|
+
or node.func.value.id not in {"cls", "self"}
|
|
253
|
+
or node.func.attr != called_name
|
|
254
|
+
):
|
|
255
|
+
continue
|
|
256
|
+
return True
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _method_nodes(method: ast.FunctionDef | ast.AsyncFunctionDef) -> list[ast.AST]:
|
|
261
|
+
nodes: list[ast.AST] = []
|
|
262
|
+
stack: list[ast.AST] = [*reversed(method.body)]
|
|
263
|
+
while stack:
|
|
264
|
+
node = stack.pop()
|
|
265
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda, ast.ClassDef)):
|
|
266
|
+
continue
|
|
267
|
+
nodes.append(node)
|
|
268
|
+
stack.extend(reversed(list(ast.iter_child_nodes(node))))
|
|
269
|
+
return nodes
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _qualified_name(node: ast.expr) -> str:
|
|
273
|
+
if isinstance(node, ast.Name):
|
|
274
|
+
return node.id
|
|
275
|
+
if isinstance(node, ast.Attribute):
|
|
276
|
+
prefix = _qualified_name(node.value)
|
|
277
|
+
return f"{prefix}.{node.attr}" if prefix else node.attr
|
|
278
|
+
return ""
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _is_none_annotation(node: ast.expr) -> bool:
|
|
282
|
+
return isinstance(node, ast.Constant) and node.value is None
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from itertools import pairwise
|
|
6
|
+
from pathlib import PurePosixPath
|
|
7
|
+
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
8
|
+
|
|
9
|
+
from sarj_python_lint.rule_base import (
|
|
10
|
+
AutofixPolicy,
|
|
11
|
+
Diagnostic,
|
|
12
|
+
ExampleFile,
|
|
13
|
+
ExampleOutcome,
|
|
14
|
+
Rule,
|
|
15
|
+
RuleCategory,
|
|
16
|
+
RuleDocumentation,
|
|
17
|
+
RuleExample,
|
|
18
|
+
Severity,
|
|
19
|
+
parse_or_none,
|
|
20
|
+
)
|
|
21
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
22
|
+
from sarj_python_lint.rules._sql import is_store_module
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@final
|
|
30
|
+
class PreferOneForRequiredRow(Rule):
|
|
31
|
+
id = "prefer-one-for-required-row"
|
|
32
|
+
code = "SARJ422"
|
|
33
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
34
|
+
summary="Use a required-row helper instead of asserting that `fetchone()` returned a row.",
|
|
35
|
+
rationale=(
|
|
36
|
+
"A bare assertion is removed by optimized Python and repeats the database contract at each call site; "
|
|
37
|
+
"a shared helper preserves the invariant and raises one intentional domain error."
|
|
38
|
+
),
|
|
39
|
+
remediation=(
|
|
40
|
+
"Wrap the fetch in the repository's required-row helper, for example "
|
|
41
|
+
"`row = one(await cursor.fetchone())`, and remove the non-None assertion."
|
|
42
|
+
),
|
|
43
|
+
category=RuleCategory.CORRECTNESS,
|
|
44
|
+
autofix=AutofixPolicy.NONE,
|
|
45
|
+
limitations=(
|
|
46
|
+
"Only an immediate exact `row is not None` assertion after `fetchone()` in production store modules is inspected.",
|
|
47
|
+
"The rule does not infer that an optional lookup must return a row and does not choose an import path for the helper.",
|
|
48
|
+
),
|
|
49
|
+
examples=(
|
|
50
|
+
RuleExample(
|
|
51
|
+
example_id="required-returning-row",
|
|
52
|
+
title="Use the shared required-row contract",
|
|
53
|
+
outcome=ExampleOutcome.MATCH,
|
|
54
|
+
files=(
|
|
55
|
+
ExampleFile.python(
|
|
56
|
+
"app/settings_store.py",
|
|
57
|
+
"async def save(cursor):\n"
|
|
58
|
+
" row = await cursor.fetchone()\n"
|
|
59
|
+
" assert row is not None, 'RETURNING must yield a row'\n"
|
|
60
|
+
" return row\n",
|
|
61
|
+
),
|
|
62
|
+
),
|
|
63
|
+
focus_path=PurePosixPath("app/settings_store.py"),
|
|
64
|
+
expected_count=1,
|
|
65
|
+
public=True,
|
|
66
|
+
),
|
|
67
|
+
RuleExample(
|
|
68
|
+
example_id="required-row-helper",
|
|
69
|
+
title="Centralize the missing-row failure",
|
|
70
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
71
|
+
files=(
|
|
72
|
+
ExampleFile.python(
|
|
73
|
+
"app/settings_store.py",
|
|
74
|
+
"async def save(cursor):\n return one(await cursor.fetchone())\n",
|
|
75
|
+
),
|
|
76
|
+
),
|
|
77
|
+
focus_path=PurePosixPath("app/settings_store.py"),
|
|
78
|
+
expected_count=0,
|
|
79
|
+
public=True,
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
description = documentation.summary
|
|
84
|
+
|
|
85
|
+
@override
|
|
86
|
+
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
87
|
+
if not is_store_module(path) or is_generated(path, source):
|
|
88
|
+
return []
|
|
89
|
+
tree = parse_or_none(path, source)
|
|
90
|
+
if tree is None:
|
|
91
|
+
return []
|
|
92
|
+
diagnostics: list[Diagnostic] = []
|
|
93
|
+
for owner in ast.walk(tree):
|
|
94
|
+
for statements in _statement_blocks(owner):
|
|
95
|
+
for assignment, assertion in pairwise(statements):
|
|
96
|
+
binding = _fetchone_binding(assignment)
|
|
97
|
+
if binding is None or not _asserts_not_none(assertion, binding.name):
|
|
98
|
+
continue
|
|
99
|
+
diagnostics.append(
|
|
100
|
+
Diagnostic(
|
|
101
|
+
path=path,
|
|
102
|
+
line=assignment.lineno,
|
|
103
|
+
col=assignment.col_offset + 1,
|
|
104
|
+
code=self.code,
|
|
105
|
+
message=(
|
|
106
|
+
f"`{binding.name}` is asserted non-None immediately after `fetchone()`. Use the shared "
|
|
107
|
+
f"required-row helper (for example `{binding.name} = one({binding.expression})`) so "
|
|
108
|
+
"optimized Python cannot remove the contract."
|
|
109
|
+
),
|
|
110
|
+
severity=Severity.WARNING,
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
return diagnostics
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _statement_blocks(node: ast.AST) -> tuple[list[ast.stmt], ...]:
|
|
117
|
+
match node:
|
|
118
|
+
case ast.Module() | ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef():
|
|
119
|
+
return (node.body,)
|
|
120
|
+
case ast.If() | ast.For() | ast.AsyncFor() | ast.While():
|
|
121
|
+
return node.body, node.orelse
|
|
122
|
+
case ast.With() | ast.AsyncWith() | ast.ExceptHandler() | ast.match_case():
|
|
123
|
+
return (node.body,)
|
|
124
|
+
case ast.Try() | ast.TryStar():
|
|
125
|
+
return node.body, node.orelse, node.finalbody, *(handler.body for handler in node.handlers)
|
|
126
|
+
case _:
|
|
127
|
+
return ()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True, slots=True)
|
|
131
|
+
class _FetchBinding:
|
|
132
|
+
name: str
|
|
133
|
+
expression: str
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _fetchone_binding(statement: ast.stmt) -> _FetchBinding | None:
|
|
137
|
+
target: ast.expr
|
|
138
|
+
value: ast.expr | None
|
|
139
|
+
if isinstance(statement, ast.Assign) and len(statement.targets) == 1:
|
|
140
|
+
target = statement.targets[0]
|
|
141
|
+
value = statement.value
|
|
142
|
+
elif isinstance(statement, ast.AnnAssign) and statement.value is not None:
|
|
143
|
+
target = statement.target
|
|
144
|
+
value = statement.value
|
|
145
|
+
else:
|
|
146
|
+
return None
|
|
147
|
+
if not isinstance(target, ast.Name):
|
|
148
|
+
return None
|
|
149
|
+
call = value.value if isinstance(value, ast.Await) else value
|
|
150
|
+
if not isinstance(call, ast.Call) or call.args or call.keywords:
|
|
151
|
+
return None
|
|
152
|
+
if not isinstance(call.func, ast.Attribute) or call.func.attr != "fetchone":
|
|
153
|
+
return None
|
|
154
|
+
return _FetchBinding(target.id, ast.unparse(value))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _asserts_not_none(statement: ast.stmt, binding: str) -> bool:
|
|
158
|
+
if not isinstance(statement, ast.Assert) or not isinstance(statement.test, ast.Compare):
|
|
159
|
+
return False
|
|
160
|
+
comparison = statement.test
|
|
161
|
+
if len(comparison.ops) != 1 or not isinstance(comparison.ops[0], ast.IsNot) or len(comparison.comparators) != 1:
|
|
162
|
+
return False
|
|
163
|
+
left, right = comparison.left, comparison.comparators[0]
|
|
164
|
+
return (_is_name(left, binding) and _is_none(right)) or (_is_none(left) and _is_name(right, binding))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _is_name(node: ast.expr, expected: str) -> bool:
|
|
168
|
+
return isinstance(node, ast.Name) and node.id == expected
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _is_none(node: ast.expr) -> bool:
|
|
172
|
+
return isinstance(node, ast.Constant) and node.value is None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
__all__ = ["PreferOneForRequiredRow"]
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from pathlib import PurePosixPath
|
|
4
|
-
import re
|
|
5
|
-
from typing import TYPE_CHECKING, ClassVar, override
|
|
6
|
-
|
|
7
|
-
from sarj_python_lint.rule_base import (
|
|
8
|
-
ColumnEncoding,
|
|
9
|
-
Diagnostic,
|
|
10
|
-
ExampleFile,
|
|
11
|
-
ExampleOutcome,
|
|
12
|
-
Rule,
|
|
13
|
-
RuleCategory,
|
|
14
|
-
RuleDocumentation,
|
|
15
|
-
RuleExample,
|
|
16
|
-
)
|
|
17
|
-
from sarj_python_lint.rules._suppression_comments import Comment, scan_comments_or_none
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if TYPE_CHECKING:
|
|
21
|
-
from pathlib import Path
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# Match directive heads using each type checker's accepted spelling. Ruff's
|
|
25
|
-
# file-level blanket is already owned by PGH004.
|
|
26
|
-
_TYPE_IGNORE_RE = re.compile(r"^type:\s*ignore(?P<rest>.*)")
|
|
27
|
-
_PYRIGHT_IGNORE_RE = re.compile(r"^pyright:\s*ignore(?P<rest>.*)")
|
|
28
|
-
|
|
29
|
-
# Require a scoped rule list after directive heads that support one.
|
|
30
|
-
_BRACKET_CODES_RE = re.compile(r"^\s*\[\s*\w")
|
|
31
|
-
|
|
32
|
-
# `type: ignored` is a different word, not the directive.
|
|
33
|
-
_WORD_CONTINUATION_RE = re.compile(r"^\w")
|
|
34
|
-
|
|
35
|
-
_TYPE_IGNORE_MESSAGE = (
|
|
36
|
-
"file-level `# type: ignore` silences every mypy error in this file, "
|
|
37
|
-
"including ones added later — scope it (`# type: ignore[attr-defined]`) "
|
|
38
|
-
"or fix the findings."
|
|
39
|
-
)
|
|
40
|
-
_PYRIGHT_IGNORE_MESSAGE = (
|
|
41
|
-
"file-level `# pyright: ignore` silences every pyright diagnostic in this file, "
|
|
42
|
-
"including ones added later — scope it (`# pyright: ignore[reportUnusedImport]`) "
|
|
43
|
-
"or fix the findings."
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class NoFileLevelSuppression(Rule):
|
|
48
|
-
id: str = "no-file-level-suppression"
|
|
49
|
-
code: str = "SARJ038"
|
|
50
|
-
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
51
|
-
summary="Unscoped file-level type-checker suppressions hide every current and future diagnostic.",
|
|
52
|
-
rationale="Mypy and Pyright blankets also hide diagnostics introduced by future tool upgrades.",
|
|
53
|
-
remediation="Fix the findings or limit the directive to the specific diagnostic codes being suppressed.",
|
|
54
|
-
category=RuleCategory.MAINTAINABILITY,
|
|
55
|
-
limitations=(
|
|
56
|
-
"Only mypy-compatible `type: ignore` and Pyright file-level directives are analyzed.",
|
|
57
|
-
"Ruff's `# ruff: noqa` blanket is owned by Ruff PGH004.",
|
|
58
|
-
"Trailing per-line suppressions and directives with an explicit code list are allowed.",
|
|
59
|
-
),
|
|
60
|
-
examples=(
|
|
61
|
-
RuleExample(
|
|
62
|
-
example_id="unscoped-type-checker-suppression",
|
|
63
|
-
title="Mypy disabled for the file",
|
|
64
|
-
outcome=ExampleOutcome.MATCH,
|
|
65
|
-
files=(ExampleFile.python("service.py", "# type: ignore\nimport os\n"),),
|
|
66
|
-
focus_path=PurePosixPath("service.py"),
|
|
67
|
-
expected_count=1,
|
|
68
|
-
public=True,
|
|
69
|
-
),
|
|
70
|
-
RuleExample(
|
|
71
|
-
example_id="scoped-type-checker-suppression",
|
|
72
|
-
title="Mypy suppression names its code",
|
|
73
|
-
outcome=ExampleOutcome.NO_MATCH,
|
|
74
|
-
files=(ExampleFile.python("service.py", "# type: ignore[import-not-found]\nimport os\n"),),
|
|
75
|
-
focus_path=PurePosixPath("service.py"),
|
|
76
|
-
expected_count=0,
|
|
77
|
-
public=True,
|
|
78
|
-
),
|
|
79
|
-
),
|
|
80
|
-
)
|
|
81
|
-
description: str = documentation.summary
|
|
82
|
-
|
|
83
|
-
@override
|
|
84
|
-
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
85
|
-
comments = scan_comments_or_none(source)
|
|
86
|
-
if comments is None:
|
|
87
|
-
return []
|
|
88
|
-
diags = [
|
|
89
|
-
Diagnostic(
|
|
90
|
-
path=path,
|
|
91
|
-
line=comment.line,
|
|
92
|
-
col=comment.col,
|
|
93
|
-
code=self.code,
|
|
94
|
-
message=message,
|
|
95
|
-
column_encoding=ColumnEncoding.CODEPOINTS,
|
|
96
|
-
)
|
|
97
|
-
for comment in comments
|
|
98
|
-
if (message := _blanket_message(comment)) is not None
|
|
99
|
-
]
|
|
100
|
-
diags.sort(key=lambda d: (d.line, d.col))
|
|
101
|
-
return diags
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def _blanket_message(comment: Comment) -> str | None:
|
|
105
|
-
if not (comment.standalone and comment.before_first_statement):
|
|
106
|
-
return None
|
|
107
|
-
if _is_unscoped(comment.body, _TYPE_IGNORE_RE, _BRACKET_CODES_RE):
|
|
108
|
-
return _TYPE_IGNORE_MESSAGE
|
|
109
|
-
if _is_unscoped(comment.body, _PYRIGHT_IGNORE_RE, _BRACKET_CODES_RE):
|
|
110
|
-
return _PYRIGHT_IGNORE_MESSAGE
|
|
111
|
-
return None
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def _is_unscoped(body: str, directive: re.Pattern[str], codes: re.Pattern[str]) -> bool:
|
|
115
|
-
match = directive.match(body)
|
|
116
|
-
if match is None:
|
|
117
|
-
return False
|
|
118
|
-
rest = match["rest"]
|
|
119
|
-
if _WORD_CONTINUATION_RE.match(rest):
|
|
120
|
-
return False
|
|
121
|
-
return not codes.match(rest)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_prose_budget.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/_test_assertions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/mock_without_spec.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_comment_cruft.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_long_comment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_secret_in_log.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_select_star.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/no_stdlib_logging.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/over_mocked_test.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_class_row.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_or_pattern.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/prefer_str_enum.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.73.2 → sarj_python_lint-0.75.0}/src/sarj_python_lint/rules/unused_mock_setup.py
RENAMED
|
File without changes
|