sarj-python-lint 0.78.2__tar.gz → 0.79.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.78.2 → sarj_python_lint-0.79.0}/PKG-INFO +1 -1
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/pyproject.toml +1 -1
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_registry.py +2 -0
- sarj_python_lint-0.79.0/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +306 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/.gitignore +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/LICENSE +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/README.md +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_long_comment.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_one_for_required_row.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/preserve_declared_nominal_id.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/preserve_enum_types.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/require_validated_row_factory.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/source_coupled_test.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/sql_requires_injected_pool_owner.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.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.79.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/
|
|
@@ -103,6 +103,7 @@ from sarj_python_lint.rules.prefer_self_documenting_constant import (
|
|
|
103
103
|
PreferSelfDocumentingConstant,
|
|
104
104
|
)
|
|
105
105
|
from sarj_python_lint.rules.prefer_self_type_annotation import PreferSelfTypeAnnotation
|
|
106
|
+
from sarj_python_lint.rules.prefer_set_isdisjoint import PreferSetIsdisjoint
|
|
106
107
|
from sarj_python_lint.rules.prefer_str_enum import PreferStrEnum
|
|
107
108
|
from sarj_python_lint.rules.prefer_struct_over_namedtuple import (
|
|
108
109
|
PreferStructOverNamedtuple,
|
|
@@ -229,6 +230,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
229
230
|
PreferWalrusComprehensionFilter.id: PreferWalrusComprehensionFilter,
|
|
230
231
|
PreferWalrusStreamLoop.id: PreferWalrusStreamLoop,
|
|
231
232
|
PreferSelfTypeAnnotation.id: PreferSelfTypeAnnotation,
|
|
233
|
+
PreferSetIsdisjoint.id: PreferSetIsdisjoint,
|
|
232
234
|
PreferSelfDocumentingConstant.id: PreferSelfDocumentingConstant,
|
|
233
235
|
NoDuplicateDunderAllEntry.id: NoDuplicateDunderAllEntry,
|
|
234
236
|
NoCopiedInheritedDocstring.id: NoCopiedInheritedDocstring,
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint.rule_base import (
|
|
8
|
+
AutofixPolicy,
|
|
9
|
+
Diagnostic,
|
|
10
|
+
ExampleFile,
|
|
11
|
+
ExampleOutcome,
|
|
12
|
+
Rule,
|
|
13
|
+
RuleCategory,
|
|
14
|
+
RuleDocumentation,
|
|
15
|
+
RuleExample,
|
|
16
|
+
is_suppressed,
|
|
17
|
+
parse_or_none,
|
|
18
|
+
)
|
|
19
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@final
|
|
27
|
+
class PreferSetIsdisjoint(Rule):
|
|
28
|
+
id = "prefer-set-isdisjoint"
|
|
29
|
+
code = "SARJ431"
|
|
30
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
31
|
+
summary="Prefer `set.isdisjoint` when a built-in set intersection is used only as a boolean predicate.",
|
|
32
|
+
rationale="`isdisjoint` names the overlap predicate directly and avoids allocating an intersection that is immediately discarded.",
|
|
33
|
+
remediation="Use `left.isdisjoint(right)` and negate it when the condition requires overlap.",
|
|
34
|
+
category=RuleCategory.STYLE,
|
|
35
|
+
autofix=AutofixPolicy.SUGGESTION,
|
|
36
|
+
limitations=(
|
|
37
|
+
"Built-in set identity must be proven from a literal, comprehension, constructor, or one dominating local assignment.",
|
|
38
|
+
"Both intersection operands must be proven built-in sets; annotations, parameters, attributes, subclasses, branch-merged bindings, stored intersections, and generated files are excluded.",
|
|
39
|
+
"The suggestion is intentionally not an autofix because short-circuiting may make custom element equality or hashing side effects observable.",
|
|
40
|
+
),
|
|
41
|
+
examples=(
|
|
42
|
+
RuleExample(
|
|
43
|
+
example_id="discarded-intersection",
|
|
44
|
+
title="Set intersection is used only for an emptiness test",
|
|
45
|
+
outcome=ExampleOutcome.MATCH,
|
|
46
|
+
files=(
|
|
47
|
+
ExampleFile.python(
|
|
48
|
+
"app/policy.py",
|
|
49
|
+
"allowed = {'read', 'write'}\nrequested = set(scopes)\nif not (allowed & requested):\n deny()\n",
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
focus_path=PurePosixPath("app/policy.py"),
|
|
53
|
+
expected_count=1,
|
|
54
|
+
public=True,
|
|
55
|
+
),
|
|
56
|
+
RuleExample(
|
|
57
|
+
example_id="named-disjoint-predicate",
|
|
58
|
+
title="Set overlap is expressed without allocating an intersection",
|
|
59
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
60
|
+
files=(
|
|
61
|
+
ExampleFile.python(
|
|
62
|
+
"app/policy.py",
|
|
63
|
+
"allowed = {'read', 'write'}\nrequested = set(scopes)\nif allowed.isdisjoint(requested):\n deny()\n",
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
focus_path=PurePosixPath("app/policy.py"),
|
|
67
|
+
expected_count=0,
|
|
68
|
+
public=True,
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
description = documentation.summary
|
|
73
|
+
|
|
74
|
+
@override
|
|
75
|
+
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
76
|
+
if ("&" not in source and ".intersection" not in source) or is_generated(path, source):
|
|
77
|
+
return []
|
|
78
|
+
tree = parse_or_none(path, source)
|
|
79
|
+
if tree is None:
|
|
80
|
+
return []
|
|
81
|
+
scanner = _Scanner(path, source.splitlines(), _shadowed_builtins(tree))
|
|
82
|
+
scanner.scan_body(tree.body, set())
|
|
83
|
+
scanner.diagnostics.sort(key=lambda item: (item.line, item.col))
|
|
84
|
+
return scanner.diagnostics
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@final
|
|
88
|
+
class _Scanner:
|
|
89
|
+
def __init__(self, path: Path, source_lines: list[str], shadowed: frozenset[str]) -> None:
|
|
90
|
+
self.path = path
|
|
91
|
+
self.source_lines = source_lines
|
|
92
|
+
self.shadowed = shadowed
|
|
93
|
+
self.diagnostics: list[Diagnostic] = []
|
|
94
|
+
self.reported: set[int] = set()
|
|
95
|
+
|
|
96
|
+
def scan_body(self, body: list[ast.stmt], exact: set[str]) -> None:
|
|
97
|
+
local = set(exact)
|
|
98
|
+
for statement in body:
|
|
99
|
+
self._scan_statement(statement, local)
|
|
100
|
+
self._update_binding(statement, local)
|
|
101
|
+
|
|
102
|
+
def _scan_statement(self, statement: ast.stmt, exact: set[str]) -> None:
|
|
103
|
+
match statement:
|
|
104
|
+
case ast.If():
|
|
105
|
+
self._scan_boolean(statement.test, exact)
|
|
106
|
+
self._scan_embedded(statement.test, exact)
|
|
107
|
+
self.scan_body(statement.body, set(exact))
|
|
108
|
+
self.scan_body(statement.orelse, set(exact))
|
|
109
|
+
case ast.While():
|
|
110
|
+
loop_exact = exact - _stored_names(ast.Module(body=[*statement.body, *statement.orelse]))
|
|
111
|
+
self._scan_boolean(statement.test, loop_exact)
|
|
112
|
+
self._scan_embedded(statement.test, loop_exact)
|
|
113
|
+
self.scan_body(statement.body, set(exact))
|
|
114
|
+
self.scan_body(statement.orelse, set(exact))
|
|
115
|
+
case ast.Assert(test=test):
|
|
116
|
+
self._scan_boolean(test, exact)
|
|
117
|
+
self._scan_embedded(test, exact)
|
|
118
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef():
|
|
119
|
+
self.scan_body(statement.body, set())
|
|
120
|
+
case ast.For() | ast.AsyncFor():
|
|
121
|
+
self._scan_embedded(statement.iter, exact)
|
|
122
|
+
nested = set(exact)
|
|
123
|
+
nested.difference_update(_stored_names(statement.target))
|
|
124
|
+
self.scan_body(statement.body, nested)
|
|
125
|
+
self.scan_body(statement.orelse, set(exact))
|
|
126
|
+
case ast.With() | ast.AsyncWith():
|
|
127
|
+
nested = set(exact)
|
|
128
|
+
for item in statement.items:
|
|
129
|
+
self._scan_embedded(item.context_expr, exact)
|
|
130
|
+
if item.optional_vars is not None:
|
|
131
|
+
nested.difference_update(_stored_names(item.optional_vars))
|
|
132
|
+
self.scan_body(statement.body, nested)
|
|
133
|
+
case ast.Try(body=body, handlers=handlers, orelse=orelse, finalbody=finalbody):
|
|
134
|
+
self.scan_body(body, set(exact))
|
|
135
|
+
for handler in handlers:
|
|
136
|
+
handler_exact = set(exact)
|
|
137
|
+
if handler.name is not None:
|
|
138
|
+
handler_exact.discard(handler.name)
|
|
139
|
+
self.scan_body(handler.body, handler_exact)
|
|
140
|
+
self.scan_body(orelse, set(exact))
|
|
141
|
+
self.scan_body(finalbody, set(exact))
|
|
142
|
+
case _:
|
|
143
|
+
for child in ast.iter_child_nodes(statement):
|
|
144
|
+
if isinstance(child, ast.expr):
|
|
145
|
+
self._scan_embedded(child, exact)
|
|
146
|
+
|
|
147
|
+
def _scan_embedded(self, expression: ast.expr, exact: set[str]) -> None:
|
|
148
|
+
if isinstance(expression, ast.Lambda):
|
|
149
|
+
nested = exact - _argument_names(expression.args)
|
|
150
|
+
self._scan_embedded(expression.body, nested)
|
|
151
|
+
return
|
|
152
|
+
if isinstance(expression, ast.IfExp):
|
|
153
|
+
self._scan_boolean(expression.test, exact)
|
|
154
|
+
if isinstance(expression, ast.DictComp):
|
|
155
|
+
nested = self._scan_comprehensions(expression.generators, exact)
|
|
156
|
+
self._scan_embedded(expression.key, nested)
|
|
157
|
+
self._scan_embedded(expression.value, nested)
|
|
158
|
+
return
|
|
159
|
+
if isinstance(expression, ast.ListComp | ast.SetComp | ast.GeneratorExp):
|
|
160
|
+
nested = self._scan_comprehensions(expression.generators, exact)
|
|
161
|
+
self._scan_embedded(expression.elt, nested)
|
|
162
|
+
return
|
|
163
|
+
for child in ast.iter_child_nodes(expression):
|
|
164
|
+
if isinstance(child, ast.expr):
|
|
165
|
+
self._scan_embedded(child, exact)
|
|
166
|
+
|
|
167
|
+
def _scan_comprehensions(self, generators: list[ast.comprehension], exact: set[str]) -> set[str]:
|
|
168
|
+
nested = set(exact)
|
|
169
|
+
for generator in generators:
|
|
170
|
+
self._scan_embedded(generator.iter, nested)
|
|
171
|
+
nested.difference_update(_stored_names(generator.target))
|
|
172
|
+
for condition in generator.ifs:
|
|
173
|
+
self._scan_boolean(condition, nested)
|
|
174
|
+
self._scan_embedded(condition, nested)
|
|
175
|
+
return nested
|
|
176
|
+
|
|
177
|
+
def _scan_boolean(self, expression: ast.expr, exact: set[str]) -> None:
|
|
178
|
+
safe_exact = exact - _named_expression_targets(expression)
|
|
179
|
+
if isinstance(expression, ast.BoolOp):
|
|
180
|
+
for value in expression.values:
|
|
181
|
+
self._scan_boolean(value, safe_exact)
|
|
182
|
+
return
|
|
183
|
+
negated = False
|
|
184
|
+
candidate = expression
|
|
185
|
+
if isinstance(expression, ast.UnaryOp) and isinstance(expression.op, ast.Not):
|
|
186
|
+
negated = True
|
|
187
|
+
candidate = expression.operand
|
|
188
|
+
if _is_intersection(candidate, safe_exact, self.shadowed):
|
|
189
|
+
self._report(candidate, negated=negated)
|
|
190
|
+
|
|
191
|
+
def _report(self, node: ast.expr, *, negated: bool) -> None:
|
|
192
|
+
if id(node) in self.reported or is_suppressed(self.source_lines, node.lineno, PreferSetIsdisjoint.code):
|
|
193
|
+
return
|
|
194
|
+
self.reported.add(id(node))
|
|
195
|
+
replacement = "`left.isdisjoint(right)`" if negated else "`not left.isdisjoint(right)`"
|
|
196
|
+
self.diagnostics.append(
|
|
197
|
+
Diagnostic(
|
|
198
|
+
path=self.path,
|
|
199
|
+
line=node.lineno,
|
|
200
|
+
col=node.col_offset + 1,
|
|
201
|
+
code=PreferSetIsdisjoint.code,
|
|
202
|
+
message=f"built-in set intersection is allocated only for a boolean test — use {replacement}",
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def _update_binding(self, statement: ast.stmt, exact: set[str]) -> None:
|
|
207
|
+
exact.difference_update(_named_expression_targets(statement))
|
|
208
|
+
match statement:
|
|
209
|
+
case ast.Assign(targets=[ast.Name(id=name)], value=value):
|
|
210
|
+
if _is_exact_set(value, exact, self.shadowed):
|
|
211
|
+
exact.add(name)
|
|
212
|
+
else:
|
|
213
|
+
exact.discard(name)
|
|
214
|
+
case ast.AnnAssign(target=ast.Name(id=name)) | ast.AugAssign(target=ast.Name(id=name)):
|
|
215
|
+
exact.discard(name)
|
|
216
|
+
case ast.Delete(targets=targets):
|
|
217
|
+
for target in targets:
|
|
218
|
+
if isinstance(target, ast.Name):
|
|
219
|
+
exact.discard(target.id)
|
|
220
|
+
case ast.Assign(targets=targets):
|
|
221
|
+
for target in targets:
|
|
222
|
+
exact.difference_update(_stored_names(target))
|
|
223
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef():
|
|
224
|
+
exact.discard(statement.name)
|
|
225
|
+
case ast.Import(names=aliases):
|
|
226
|
+
exact.difference_update(alias.asname or alias.name.split(".")[0] for alias in aliases)
|
|
227
|
+
case ast.ImportFrom(names=aliases):
|
|
228
|
+
if any(alias.name == "*" for alias in aliases):
|
|
229
|
+
exact.clear()
|
|
230
|
+
else:
|
|
231
|
+
exact.difference_update(alias.asname or alias.name for alias in aliases)
|
|
232
|
+
case (
|
|
233
|
+
ast.If()
|
|
234
|
+
| ast.While()
|
|
235
|
+
| ast.For()
|
|
236
|
+
| ast.AsyncFor()
|
|
237
|
+
| ast.With()
|
|
238
|
+
| ast.AsyncWith()
|
|
239
|
+
| ast.Try()
|
|
240
|
+
| ast.Match()
|
|
241
|
+
):
|
|
242
|
+
exact.difference_update(_stored_names(statement))
|
|
243
|
+
case _:
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _is_intersection(node: ast.expr, exact: set[str], shadowed: frozenset[str]) -> bool:
|
|
248
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitAnd):
|
|
249
|
+
return _is_exact_set(node.left, exact, shadowed) and _is_exact_set(node.right, exact, shadowed)
|
|
250
|
+
return (
|
|
251
|
+
isinstance(node, ast.Call)
|
|
252
|
+
and isinstance(node.func, ast.Attribute)
|
|
253
|
+
and node.func.attr == "intersection"
|
|
254
|
+
and len(node.args) == 1
|
|
255
|
+
and not node.keywords
|
|
256
|
+
and _is_exact_set(node.func.value, exact, shadowed)
|
|
257
|
+
and _is_exact_set(node.args[0], exact, shadowed)
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _is_exact_set(node: ast.expr, exact: set[str], shadowed: frozenset[str]) -> bool:
|
|
262
|
+
if isinstance(node, ast.Set | ast.SetComp):
|
|
263
|
+
return True
|
|
264
|
+
if isinstance(node, ast.Name):
|
|
265
|
+
return node.id in exact
|
|
266
|
+
return (
|
|
267
|
+
isinstance(node, ast.Call)
|
|
268
|
+
and isinstance(node.func, ast.Name)
|
|
269
|
+
and node.func.id in {"set", "frozenset"}
|
|
270
|
+
and node.func.id not in shadowed
|
|
271
|
+
and len(node.args) <= 1
|
|
272
|
+
and not node.keywords
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _stored_names(node: ast.AST) -> set[str]:
|
|
277
|
+
return {child.id for child in ast.walk(node) if isinstance(child, ast.Name) and isinstance(child.ctx, ast.Store)}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _named_expression_targets(node: ast.AST) -> set[str]:
|
|
281
|
+
return {expression.target.id for expression in ast.walk(node) if isinstance(expression, ast.NamedExpr)}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _argument_names(arguments: ast.arguments) -> set[str]:
|
|
285
|
+
return {
|
|
286
|
+
argument.arg
|
|
287
|
+
for argument in (
|
|
288
|
+
*arguments.posonlyargs,
|
|
289
|
+
*arguments.args,
|
|
290
|
+
*arguments.kwonlyargs,
|
|
291
|
+
*((arguments.vararg,) if arguments.vararg is not None else ()),
|
|
292
|
+
*((arguments.kwarg,) if arguments.kwarg is not None else ()),
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _shadowed_builtins(tree: ast.Module) -> frozenset[str]:
|
|
298
|
+
shadowed: set[str] = set()
|
|
299
|
+
for node in ast.walk(tree):
|
|
300
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store) and node.id in {"set", "frozenset"}:
|
|
301
|
+
shadowed.add(node.id)
|
|
302
|
+
elif isinstance(node, ast.arg) and node.arg in {"set", "frozenset"}:
|
|
303
|
+
shadowed.add(node.arg)
|
|
304
|
+
elif isinstance(node, ast.alias) and (node.asname or node.name.split(".")[0]) in {"set", "frozenset"}:
|
|
305
|
+
shadowed.add(node.asname or node.name.split(".")[0])
|
|
306
|
+
return frozenset(shadowed)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/_analysis_session.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
|
{sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.78.2 → sarj_python_lint-0.79.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.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.78.2 → sarj_python_lint-0.79.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.78.2 → sarj_python_lint-0.79.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
|
{sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/mock_without_spec.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.78.2 → sarj_python_lint-0.79.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
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.78.2 → sarj_python_lint-0.79.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_secret_in_log.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_select_star.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.78.2 → sarj_python_lint-0.79.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.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/over_mocked_test.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.78.2 → sarj_python_lint-0.79.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
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_or_pattern.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.78.2 → sarj_python_lint-0.79.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/unused_mock_setup.py
RENAMED
|
File without changes
|