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.
Files changed (127) hide show
  1. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_registry.py +2 -0
  4. sarj_python_lint-0.79.0/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +306 -0
  5. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/.gitignore +0 -0
  6. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/LICENSE +0 -0
  7. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/README.md +0 -0
  8. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/__init__.py +0 -0
  9. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/__main__.py +0 -0
  10. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  11. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/_filesystem.py +0 -0
  12. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  13. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/_secret_names.py +0 -0
  14. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/_version.py +0 -0
  15. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/py.typed +0 -0
  16. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/ratchet.py +0 -0
  17. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rule_base.py +0 -0
  18. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  19. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  20. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  21. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  22. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  23. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  24. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  25. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  26. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  27. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  28. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  29. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  30. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  31. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  32. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  33. {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
  34. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  35. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  36. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  37. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  38. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  39. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  40. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  41. {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
  42. {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
  43. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  44. {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
  45. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  46. {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
  47. {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
  48. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  49. {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
  50. {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
  51. {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
  52. {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
  53. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  54. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
  55. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_long_comment.py +0 -0
  56. {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
  57. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  58. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
  59. {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
  60. {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
  61. {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
  62. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  63. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  64. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  65. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  66. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  67. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  68. {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
  69. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
  70. {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
  71. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  72. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  73. {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
  74. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  75. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  76. {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
  77. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  78. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  79. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  80. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  81. {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
  82. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  83. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  84. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  85. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  86. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  87. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  88. {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
  89. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  90. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  91. {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
  92. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  93. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  94. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  95. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  96. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  97. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
  98. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  99. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  100. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  101. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/preserve_declared_nominal_id.py +0 -0
  102. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/preserve_enum_types.py +0 -0
  103. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  104. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
  105. {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
  106. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  107. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  108. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  109. {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
  110. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  111. {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
  112. {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
  113. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  114. {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
  115. {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
  116. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/require_validated_row_factory.py +0 -0
  117. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  118. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/source_coupled_test.py +0 -0
  119. {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
  120. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  121. {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
  122. {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
  123. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  124. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  125. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
  126. {sarj_python_lint-0.78.2 → sarj_python_lint-0.79.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  127. {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.78.2
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/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "0.78.2"
3
+ version = "0.79.0"
4
4
  description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "sarj-ai" }]
@@ -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)