sarj-python-lint 0.88.0__tar.gz → 1.1.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 (147) hide show
  1. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/.gitignore +1 -1
  2. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/PKG-INFO +2 -1
  3. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/pyproject.toml +5 -2
  4. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/__main__.py +11 -6
  5. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/_analysis_session.py +6 -1
  6. sarj_python_lint-1.1.0/src/sarj_python_lint/_file_context.py +68 -0
  7. sarj_python_lint-1.1.0/src/sarj_python_lint/benchmark.py +118 -0
  8. sarj_python_lint-1.1.0/src/sarj_python_lint/rule_base.py +158 -0
  9. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_ast_index.py +10 -13
  10. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_fastapi.py +4 -3
  11. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_fixed_record.py +11 -9
  12. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_imports.py +3 -1
  13. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_project_index.py +4 -3
  14. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_prose_budget.py +3 -3
  15. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +13 -11
  16. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +56 -35
  17. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +8 -8
  18. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +8 -7
  19. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +8 -7
  20. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/excessive_commentary.py +10 -7
  21. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +7 -5
  22. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +24 -13
  23. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +12 -12
  24. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +8 -9
  25. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +12 -8
  26. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/mock_without_spec.py +29 -13
  27. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +17 -19
  28. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +9 -8
  29. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +12 -11
  30. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +23 -12
  31. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_comment_cruft.py +6 -3
  32. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +5 -6
  33. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +6 -5
  34. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +8 -8
  35. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_delete_statement.py +8 -8
  36. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_dunder_all.py +6 -6
  37. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +7 -7
  38. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +7 -7
  39. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +7 -5
  40. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +4 -2
  41. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +27 -17
  42. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +9 -7
  43. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +9 -9
  44. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +9 -7
  45. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +8 -12
  46. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +5 -4
  47. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +12 -10
  48. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_offset_pagination.py +10 -10
  49. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +15 -11
  50. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +15 -10
  51. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +6 -8
  52. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +10 -7
  53. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +13 -12
  54. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +9 -8
  55. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +8 -8
  56. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +7 -5
  57. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +8 -6
  58. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_restated_comment.py +14 -14
  59. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_secret_in_log.py +6 -5
  60. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_select_star.py +13 -12
  61. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +11 -9
  62. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +7 -6
  63. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_tautological_expect.py +13 -12
  64. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +8 -3
  65. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +18 -13
  66. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +46 -24
  67. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +12 -11
  68. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +5 -5
  69. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +15 -10
  70. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +21 -13
  71. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/over_mocked_test.py +7 -5
  72. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/phase_label_comment.py +6 -5
  73. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_class_row.py +16 -9
  74. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +23 -18
  75. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +18 -15
  76. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +11 -11
  77. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +6 -6
  78. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +21 -14
  79. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_library_fake.py +7 -5
  80. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +17 -13
  81. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +30 -17
  82. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +10 -9
  83. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +17 -16
  84. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +25 -22
  85. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +20 -13
  86. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +18 -11
  87. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +15 -11
  88. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +17 -13
  89. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +8 -8
  90. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +9 -7
  91. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +21 -15
  92. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +22 -19
  93. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +14 -10
  94. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_str_enum.py +46 -27
  95. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +23 -14
  96. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +13 -12
  97. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +12 -9
  98. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +16 -11
  99. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +14 -10
  100. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +10 -7
  101. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +9 -9
  102. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +12 -12
  103. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/redundant_docstring.py +17 -13
  104. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +14 -9
  105. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +8 -8
  106. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +9 -7
  107. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +31 -24
  108. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +12 -9
  109. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/require_port_for_service.py +31 -20
  110. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +34 -20
  111. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +16 -14
  112. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +11 -8
  113. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/restated_test_docstring.py +20 -15
  114. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/stepdown.py +7 -5
  115. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +7 -7
  116. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +13 -14
  117. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +7 -8
  118. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/trailing_value_narration.py +12 -10
  119. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/typed_error_reasons.py +11 -8
  120. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +7 -5
  121. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/unused_mock_setup.py +11 -10
  122. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +46 -24
  123. sarj_python_lint-0.88.0/src/sarj_python_lint/rule_base.py +0 -321
  124. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/LICENSE +0 -0
  125. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/README.md +0 -0
  126. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/__init__.py +0 -0
  127. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/_filesystem.py +0 -0
  128. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/_python_target.py +0 -0
  129. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  130. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/_secret_names.py +0 -0
  131. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/_version.py +0 -0
  132. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/json_boundary.py +0 -0
  133. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/py.typed +0 -0
  134. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/ratchet.py +0 -0
  135. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  136. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  137. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  138. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  139. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  140. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  141. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  142. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  143. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  144. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  145. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  146. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  147. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
@@ -1,4 +1,4 @@
1
- node_modules/
1
+ node_modules
2
2
  .astro/
3
3
  dist/
4
4
  build/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 0.88.0
3
+ Version: 1.1.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 @@ Classifier: Topic :: Software Development :: Quality Assurance
19
19
  Requires-Python: >=3.14
20
20
  Requires-Dist: packaging==26.3
21
21
  Requires-Dist: ruff>=0.16.8
22
+ Requires-Dist: sarj-rule-contracts==0.1.0
22
23
  Requires-Dist: sqlglot==30.19.0
23
24
  Requires-Dist: typer==0.27.2
24
25
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "0.88.0"
3
+ version = "1.1.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" }]
@@ -15,7 +15,7 @@ classifiers = [
15
15
  "Programming Language :: Python :: 3.14",
16
16
  "Topic :: Software Development :: Quality Assurance",
17
17
  ]
18
- dependencies = ["packaging==26.3", "ruff>=0.16.8", "sqlglot==30.19.0", "typer==0.27.2"]
18
+ dependencies = ["sarj-rule-contracts==0.1.0", "packaging==26.3", "ruff>=0.16.8", "sqlglot==30.19.0", "typer==0.27.2"]
19
19
 
20
20
  [project.scripts]
21
21
  sarj-python-lint = "sarj_python_lint.__main__:main"
@@ -69,3 +69,6 @@ extend = "../../.ruff-strict.toml"
69
69
  # Ruff is invoked at runtime through `sys.executable -m ruff`, which Deptry's
70
70
  # import analysis cannot observe.
71
71
  DEP002 = ["ruff"]
72
+
73
+ [tool.uv.sources]
74
+ sarj-rule-contracts = { path = "../contracts", editable = true }
@@ -13,6 +13,7 @@ import typer
13
13
 
14
14
  from sarj_python_lint import __version__
15
15
  from sarj_python_lint._analysis_session import AnalysisSession
16
+ from sarj_python_lint._file_context import PythonFileContext
16
17
  from sarj_python_lint._filesystem import atomic_write_text
17
18
  from sarj_python_lint.json_boundary import is_object_mapping, parse_json
18
19
  from sarj_python_lint.rule_base import Diagnostic, ProjectRule, Rule, Severity, is_suppressed
@@ -47,7 +48,7 @@ SKIP_DIR_NAMES = frozenset(
47
48
  _MAX_FILE_BYTES = 500_000
48
49
 
49
50
 
50
- def _expand_paths(paths: list[Path]) -> list[Path]:
51
+ def expand_paths(paths: list[Path]) -> list[Path]:
51
52
  out: list[Path] = []
52
53
  for p in paths:
53
54
  if not p.exists():
@@ -91,7 +92,7 @@ def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
91
92
  for rule in rules:
92
93
  rule.prepare_session(session)
93
94
  clear_path_caches()
94
- expanded = _expand_paths(paths)
95
+ expanded = expand_paths(paths)
95
96
  loaded: dict[Path, str] = {}
96
97
  for path in expanded:
97
98
  try:
@@ -101,17 +102,21 @@ def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
101
102
  project_rules = [rule for rule in rules if isinstance(rule, ProjectRule)]
102
103
  if project_rules:
103
104
  indexes = ProjectIndexSet.build(expanded, loaded, facts=session.first_party)
105
+ session.project = indexes
104
106
  for rule in project_rules:
105
107
  rule.prepare(indexes)
106
108
  diags: list[Diagnostic] = []
107
109
  for p, source in loaded.items():
108
- diags.extend(_check_source(rules, p, source))
110
+ diags.extend(check_source(rules, p, source, session=session))
109
111
  return diags
110
112
 
111
113
 
112
- def _check_source(rules: list[Rule], p: Path, source: str) -> list[Diagnostic]:
113
- source_lines = source.splitlines()
114
- raw = [diagnostic for rule in rules for diagnostic in rule.check(p, source)]
114
+ def check_source(
115
+ rules: list[Rule], p: Path, source: str, *, session: AnalysisSession | None = None
116
+ ) -> list[Diagnostic]:
117
+ context = PythonFileContext(p, source, session)
118
+ source_lines = context.source_lines
119
+ raw = [diagnostic for rule in rules for diagnostic in rule.check_context(context)]
115
120
  return deduplicate_diagnostics(
116
121
  [
117
122
  diagnostic
@@ -1,13 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import final
3
+ from typing import TYPE_CHECKING, final
4
4
 
5
5
  from sarj_python_lint._python_target import PythonTargetFacts
6
6
  from sarj_python_lint.rules._first_party import FirstPartyFacts
7
7
 
8
8
 
9
+ if TYPE_CHECKING:
10
+ from sarj_python_lint.rules._project_index import ProjectIndexSet
11
+
12
+
9
13
  @final
10
14
  class AnalysisSession:
11
15
  def __init__(self) -> None:
16
+ self.project: ProjectIndexSet | None = None
12
17
  self.first_party = FirstPartyFacts()
13
18
  self.python_target = PythonTargetFacts()
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from functools import cached_property
5
+ from typing import TYPE_CHECKING, final
6
+
7
+ from sarj_python_lint._analysis_session import AnalysisSession
8
+ from sarj_python_lint.rules._ast_index import NodeIndex
9
+ from sarj_python_lint.rules._fastapi import FastapiIndex
10
+ from sarj_python_lint.rules._imports import ImportIndex
11
+ from sarj_python_lint.rules._paths import is_generated
12
+
13
+
14
+ if TYPE_CHECKING:
15
+ from pathlib import Path
16
+
17
+
18
+ @final
19
+ class PythonFileContext:
20
+ def __init__(self, path: Path, source: str, session: AnalysisSession | None = None) -> None:
21
+ self.path = path
22
+ self.source = source
23
+ self.session = session if session is not None else AnalysisSession()
24
+
25
+ @cached_property
26
+ def tree(self) -> ast.Module | None:
27
+ if self.session.project is not None:
28
+ unit = self.session.project.unit(self.path)
29
+ if unit is not None and unit.source == self.source:
30
+ return unit.tree
31
+ try:
32
+ return ast.parse(self.source, filename=str(self.path))
33
+ except SyntaxError:
34
+ return None
35
+
36
+ @cached_property
37
+ def source_lines(self) -> list[str]:
38
+ return self.source.splitlines()
39
+
40
+ @cached_property
41
+ def generated(self) -> bool:
42
+ return is_generated(self.path, self.source)
43
+
44
+ @cached_property
45
+ def node_index(self) -> NodeIndex:
46
+ return NodeIndex(self._valid_tree())
47
+
48
+ def nodes[NodeT: ast.AST](self, *types: type[NodeT]) -> list[NodeT]:
49
+ return [node for node in self.node_index.query(types) if isinstance(node, types)]
50
+
51
+ @cached_property
52
+ def imports(self) -> ImportIndex:
53
+ return ImportIndex.from_tree(self._valid_tree())
54
+
55
+ @cached_property
56
+ def module_imports(self) -> ImportIndex:
57
+ return ImportIndex.from_tree(self._valid_tree(), module_scope_only=True)
58
+
59
+ @cached_property
60
+ def fastapi(self) -> FastapiIndex:
61
+ return FastapiIndex(self._valid_tree(), path=self.path)
62
+
63
+ def _valid_tree(self) -> ast.Module:
64
+ tree = self.tree
65
+ if tree is None:
66
+ msg = "syntax-dependent facts require a successfully parsed source file"
67
+ raise ValueError(msg)
68
+ return tree
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass
4
+ import hashlib
5
+ import json
6
+ from os.path import commonpath
7
+ from pathlib import Path
8
+ from statistics import median
9
+ import sys
10
+ from time import perf_counter
11
+ from typing import Annotated
12
+
13
+ import typer
14
+
15
+ from sarj_python_lint.__main__ import analyze, expand_paths
16
+ from sarj_python_lint.rules import REGISTRY
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class BenchmarkSample:
21
+ seconds: float
22
+ diagnostics: int
23
+ diagnostic_digest: str
24
+ peak_memory_bytes: int | None
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class BenchmarkReport:
29
+ files: int
30
+ source_bytes: int
31
+ source_digest: str
32
+ rules: tuple[str, ...]
33
+ samples: tuple[BenchmarkSample, ...]
34
+ median_seconds: float
35
+
36
+
37
+ def peak_memory_bytes() -> int | None:
38
+ try:
39
+ import resource # ruff: ignore[import-outside-top-level] — optional Unix-only memory instrumentation.
40
+ except ImportError:
41
+ return None
42
+ peak = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
43
+ return peak if sys.platform == "darwin" else peak * 1024
44
+
45
+
46
+ def measure(paths: list[Path], rule_ids: list[str] | None = None, *, repeats: int = 5) -> BenchmarkReport:
47
+ if repeats < 1:
48
+ msg = "repeats must be positive"
49
+ raise ValueError(msg)
50
+ selected = sorted(REGISTRY) if rule_ids is None else sorted(rule_ids)
51
+ unknown = sorted(set(selected) - REGISTRY.keys())
52
+ if unknown:
53
+ msg = f"unknown rule(s): {', '.join(unknown)}"
54
+ raise ValueError(msg)
55
+ expanded = sorted(set(expand_paths(paths)))
56
+ if not expanded:
57
+ msg = "benchmark requires at least one Python source file"
58
+ raise ValueError(msg)
59
+ corpus_root = Path(commonpath([path.absolute().parent for path in expanded]))
60
+ source_digest = hashlib.sha256()
61
+ source_bytes = 0
62
+ for path in expanded:
63
+ source = path.read_bytes()
64
+ source_bytes += len(source)
65
+ source_digest.update(str(path.absolute().relative_to(corpus_root)).encode())
66
+ source_digest.update(b"\0")
67
+ source_digest.update(source)
68
+ source_digest.update(b"\0")
69
+ samples: list[BenchmarkSample] = []
70
+ for _ in range(repeats):
71
+ start = perf_counter()
72
+ diagnostics = analyze(selected, expanded)
73
+ elapsed = perf_counter() - start
74
+ digest = hashlib.sha256()
75
+ for diagnostic in diagnostics:
76
+ digest.update(
77
+ json.dumps(
78
+ [
79
+ str(diagnostic.path.absolute().relative_to(corpus_root)),
80
+ diagnostic.line,
81
+ diagnostic.col,
82
+ diagnostic.code,
83
+ diagnostic.message,
84
+ diagnostic.severity.value,
85
+ diagnostic.column_encoding.value,
86
+ ],
87
+ ensure_ascii=False,
88
+ ).encode()
89
+ )
90
+ digest.update(b"\n")
91
+ samples.append(BenchmarkSample(elapsed, len(diagnostics), digest.hexdigest(), peak_memory_bytes()))
92
+ if len({sample.diagnostic_digest for sample in samples}) != 1:
93
+ msg = "diagnostics changed between benchmark repetitions"
94
+ raise RuntimeError(msg)
95
+ return BenchmarkReport(
96
+ len(expanded),
97
+ source_bytes,
98
+ source_digest.hexdigest(),
99
+ tuple(selected),
100
+ tuple(samples),
101
+ median(sample.seconds for sample in samples),
102
+ )
103
+
104
+
105
+ def _run(
106
+ paths: Annotated[list[Path], typer.Argument(help="Python files or directories to analyze.")],
107
+ repeats: Annotated[int, typer.Option(min=1)] = 5,
108
+ rule: Annotated[list[str] | None, typer.Option(help="Repeat to select rules; defaults to all rules.")] = None,
109
+ ) -> None:
110
+ try:
111
+ report = measure(paths, rule, repeats=repeats)
112
+ except ValueError as exc:
113
+ raise typer.BadParameter(str(exc)) from exc
114
+ typer.echo(json.dumps(asdict(report), indent=2))
115
+
116
+
117
+ if __name__ == "__main__":
118
+ typer.run(_run)
@@ -0,0 +1,158 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ import ast
5
+ from dataclasses import dataclass
6
+ from enum import StrEnum
7
+ from pathlib import Path
8
+ import re
9
+ from typing import TYPE_CHECKING, ClassVar, Final
10
+
11
+ from sarj_rule_contracts import (
12
+ AutofixPolicy as AutofixPolicy,
13
+ ExampleFile as ExampleFile,
14
+ ExampleOutcome as ExampleOutcome,
15
+ NativeRuleSpec as NativeRuleSpec,
16
+ RuleCategory as RuleCategory,
17
+ RuleDocumentation as RuleDocumentation,
18
+ RuleExample as RuleExample,
19
+ Severity as Severity,
20
+ )
21
+
22
+ from sarj_python_lint._python_target import PythonTargetFacts
23
+
24
+
25
+ if TYPE_CHECKING:
26
+ from collections.abc import Sequence
27
+
28
+ from sarj_python_lint._analysis_session import AnalysisSession
29
+ from sarj_python_lint._file_context import PythonFileContext
30
+ from sarj_python_lint.rules._project_index import ProjectIndexSet
31
+
32
+
33
+ # Each rule points directly to its executable examples.
34
+ REPO_BLOB: Final = "https://github.com/sarj-ai/code-standards/blob/main"
35
+ TESTS_DIR: Final = "packages/python/tests/rules"
36
+
37
+
38
+ # Keep SARJ suppressions separate because Ruff removes unknown `noqa` codes.
39
+ _SARJ_NOQA_RE = re.compile(
40
+ r"#\s*sarj-noqa(?::\s*([A-Za-z0-9_, ]+))?",
41
+ re.IGNORECASE,
42
+ )
43
+
44
+
45
+ def is_suppressed(source_lines: Sequence[str], line: int, code: str) -> bool:
46
+ if line < 1 or line > len(source_lines):
47
+ return False
48
+ text = source_lines[line - 1]
49
+ m = _SARJ_NOQA_RE.search(text)
50
+ if not m:
51
+ return False
52
+ codes_str = m.group(1)
53
+ if not codes_str:
54
+ # A bare sarj-noqa intentionally suppresses every SARJ code on its line.
55
+ return True
56
+ codes = {val.upper() for c in codes_str.split(",") if (val := c.strip())}
57
+ return code.upper() in codes
58
+
59
+
60
+ class ColumnEncoding(StrEnum):
61
+ UTF8_BYTES = "utf8-bytes"
62
+ CODEPOINTS = "codepoints"
63
+
64
+
65
+ @dataclass(frozen=True, slots=True)
66
+ class Diagnostic:
67
+ path: Path
68
+ line: int
69
+ col: int
70
+ code: str
71
+ message: str
72
+ severity: Severity = Severity.ERROR
73
+ column_encoding: ColumnEncoding = ColumnEncoding.UTF8_BYTES
74
+
75
+ def format(self) -> str:
76
+ label = "warning: " if self.severity is Severity.WARNING else ""
77
+ return f"{self.path}:{self.line}:{self.col}: {self.code} {label}{self.message}"
78
+
79
+
80
+ class Rule(ABC):
81
+ id: str
82
+ code: str
83
+ description: str
84
+ documentation: ClassVar[RuleDocumentation | None] = None
85
+ _analysis_session: AnalysisSession | None = None
86
+
87
+ def prepare_session(self, session: AnalysisSession) -> None:
88
+ self._analysis_session = session
89
+
90
+ def has_declared_python_support_before(self, path: Path, minimum: tuple[int, int]) -> bool:
91
+ facts = self._analysis_session.python_target if self._analysis_session is not None else PythonTargetFacts()
92
+ return facts.has_declared_support_before(path, minimum)
93
+
94
+ def check(self, path: Path, source: str) -> list[Diagnostic]:
95
+ from sarj_python_lint._file_context import PythonFileContext # ruff: ignore[import-outside-top-level] — avoids registry import cycle
96
+
97
+ context = PythonFileContext(path, source, self._analysis_session)
98
+ if isinstance(self, ProjectRule):
99
+ context.session.project = self.project_indexes
100
+ return self.check_context(context)
101
+
102
+ @abstractmethod
103
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
104
+ raise NotImplementedError
105
+
106
+ @classmethod
107
+ def examples_path(cls) -> str:
108
+ return f"{TESTS_DIR}/test_{cls.__module__.rpartition('.')[2]}.py"
109
+
110
+ @classmethod
111
+ def examples_url(cls) -> str:
112
+ return f"{REPO_BLOB}/{cls.examples_path()}"
113
+
114
+ @classmethod
115
+ def native_spec(cls) -> NativeRuleSpec | None:
116
+ authored = cls.documentation
117
+ if authored is None:
118
+ return None
119
+ if cls.id in authored.aliases:
120
+ msg = f"{cls.id}: a live rule ID cannot also be a historical alias"
121
+ raise ValueError(msg)
122
+ if authored.summary != cls.description:
123
+ msg = f"{cls.id}: description must be the authored documentation summary"
124
+ raise ValueError(msg)
125
+ return NativeRuleSpec(
126
+ engine="python",
127
+ rule_id=cls.id,
128
+ code=cls.code,
129
+ summary=authored.summary,
130
+ rationale=authored.rationale,
131
+ remediation=authored.remediation,
132
+ category=authored.category,
133
+ default_level=authored.default_level,
134
+ autofix=authored.autofix,
135
+ aliases=authored.aliases,
136
+ limitations=authored.limitations,
137
+ examples=authored.examples,
138
+ )
139
+
140
+ @classmethod
141
+ def public_examples(cls) -> tuple[RuleExample, ...]:
142
+ spec = cls.native_spec()
143
+ return () if spec is None else spec.public_examples
144
+
145
+
146
+ class ProjectRule(Rule, ABC):
147
+ project_indexes: ProjectIndexSet | None = None
148
+
149
+ def prepare(self, indexes: ProjectIndexSet) -> None:
150
+ self.project_indexes = indexes
151
+
152
+
153
+ def parse_or_none(path: Path, source: str) -> ast.Module | None:
154
+ try:
155
+ tree = ast.parse(source, filename=str(path))
156
+ except SyntaxError:
157
+ tree = None
158
+ return tree
@@ -28,7 +28,10 @@ def children(node: ast.AST) -> list[ast.AST]:
28
28
  return out
29
29
 
30
30
 
31
- def walk(node: ast.AST) -> Iterator[ast.AST]:
31
+ def walk(node: ast.AST, *, index: NodeIndex | None = None) -> Iterator[ast.AST]:
32
+ if index is not None and index.tree is node:
33
+ yield from index.query((ast.AST,))
34
+ return
32
35
  queue: list[ast.AST] = [node]
33
36
  i = 0
34
37
  while i < len(queue):
@@ -39,10 +42,11 @@ def walk(node: ast.AST) -> Iterator[ast.AST]:
39
42
 
40
43
 
41
44
  @final
42
- class _NodeIndex:
43
- __slots__ = ("_buckets", "_flat", "_queries")
45
+ class NodeIndex:
46
+ __slots__ = ("_buckets", "_flat", "_queries", "tree")
44
47
 
45
48
  def __init__(self, tree: ast.AST) -> None:
49
+ self.tree = tree
46
50
  buckets: dict[type[ast.AST], list[ast.AST]] = {}
47
51
  flat: list[ast.AST] = [tree]
48
52
  i = 0
@@ -80,13 +84,6 @@ class _NodeIndex:
80
84
  return result
81
85
 
82
86
 
83
- _last_index: tuple[ast.AST, _NodeIndex] | None = None
84
- # The strong tree reference prevents a recycled object id from ever reusing a stale index.
85
-
86
-
87
- def nodes[NodeT: ast.AST](tree: ast.AST, *types: type[NodeT]) -> list[NodeT]:
88
- global _last_index # ruff: ignore[global-statement] — single-slot memo, mirroring `parse_or_none`
89
- if _last_index is None or _last_index[0] is not tree:
90
- _last_index = (tree, _NodeIndex(tree))
91
- # The `isinstance` pass is what narrows `list[ast.AST]` to `list[NodeT]`
92
- return [node for node in _last_index[1].query(types) if isinstance(node, types)]
87
+ def nodes[NodeT: ast.AST](tree: ast.AST, *types: type[NodeT], index: NodeIndex | None = None) -> list[NodeT]:
88
+ selected = index if index is not None and index.tree is tree else NodeIndex(tree)
89
+ return [node for node in selected.query(types) if isinstance(node, types)]
@@ -5,6 +5,7 @@ from collections import Counter
5
5
  from dataclasses import dataclass
6
6
  from typing import TYPE_CHECKING, Literal, NamedTuple
7
7
 
8
+ from sarj_python_lint.rules._ast_index import walk as walk_ast
8
9
  from sarj_python_lint.rules._imports import ImportIndex
9
10
 
10
11
 
@@ -153,7 +154,7 @@ class FastapiIndex:
153
154
  visit(tree, 0, None)
154
155
 
155
156
  def _read_imports(self, tree: ast.Module) -> None:
156
- for node in ast.walk(tree):
157
+ for node in walk_ast(tree):
157
158
  if self._node_scopes[id(node)] != 0:
158
159
  continue
159
160
  if isinstance(node, ast.Import):
@@ -197,7 +198,7 @@ class FastapiIndex:
197
198
 
198
199
  def _read_aliases(self, tree: ast.Module) -> None:
199
200
  assignments: list[tuple[int, str, ast.expr]] = []
200
- for node in ast.walk(tree):
201
+ for node in walk_ast(tree):
201
202
  self._read_bound_names(node)
202
203
  assignment = self._alias_assignment(node)
203
204
  if assignment is not None:
@@ -638,7 +639,7 @@ def _statement_binds(statement: ast.stmt, name: str) -> bool:
638
639
 
639
640
  def _stored_names(node: ast.expr) -> frozenset[str]:
640
641
  return frozenset(
641
- child.id for child in ast.walk(node) if isinstance(child, ast.Name) and isinstance(child.ctx, ast.Store)
642
+ child.id for child in walk_ast(node) if isinstance(child, ast.Name) and isinstance(child.ctx, ast.Store)
642
643
  )
643
644
 
644
645
 
@@ -5,6 +5,8 @@ from dataclasses import dataclass, field
5
5
  from enum import Enum, auto
6
6
  from typing import TYPE_CHECKING
7
7
 
8
+ from sarj_python_lint.rules._ast_index import walk as walk_ast
9
+
8
10
 
9
11
  if TYPE_CHECKING:
10
12
  from sarj_python_lint.rules._imports import ImportIndex
@@ -164,7 +166,7 @@ class _Analyzer:
164
166
 
165
167
  def _loop_statement(self, statement: ast.stmt, state: _State) -> _Flow:
166
168
  self.invalidate_loop(statement, state)
167
- has_return = any(isinstance(child, ast.Return) for child in ast.walk(statement))
169
+ has_return = any(isinstance(child, ast.Return) for child in walk_ast(statement))
168
170
  returned = [_ReturnKind.UNKNOWN] if has_return else []
169
171
  return _Flow(active=[state], returned=returned)
170
172
 
@@ -173,7 +175,7 @@ class _Analyzer:
173
175
  state.detach(name)
174
176
 
175
177
  def _inspect_fallback(self, statement: ast.stmt, state: _State) -> None:
176
- for child in ast.walk(statement):
178
+ for child in walk_ast(statement):
177
179
  match child:
178
180
  case ast.Call():
179
181
  self.call(child, state)
@@ -424,7 +426,7 @@ class _Analyzer:
424
426
  def escape_expression(self, value: ast.expr, state: _State) -> None:
425
427
  if isinstance(value, (ast.Lambda, ast.Dict, ast.List, ast.Set, ast.Tuple)):
426
428
  self._mark_loaded_records_open(value, state)
427
- for child in ast.walk(value):
429
+ for child in walk_ast(value):
428
430
  match child:
429
431
  case ast.Call():
430
432
  self.call(child, state)
@@ -455,7 +457,7 @@ class _Analyzer:
455
457
  state.detach(name)
456
458
 
457
459
  def invalidate_loop(self, node: ast.stmt, state: _State) -> None:
458
- for child in ast.walk(node):
460
+ for child in walk_ast(node):
459
461
  match child:
460
462
  case ast.Name(id=name, ctx=ast.Store()):
461
463
  record = state.record(name)
@@ -504,13 +506,13 @@ def _root_name(node: ast.expr) -> str | None:
504
506
 
505
507
 
506
508
  def _loaded_names(node: ast.AST) -> set[str]:
507
- return {child.id for child in ast.walk(node) if isinstance(child, ast.Name) and isinstance(child.ctx, ast.Load)}
509
+ return {child.id for child in walk_ast(node) if isinstance(child, ast.Name) and isinstance(child.ctx, ast.Load)}
508
510
 
509
511
 
510
512
  def _stored_names(node: ast.AST) -> set[str]:
511
513
  return {
512
514
  child.id
513
- for child in ast.walk(node)
515
+ for child in walk_ast(node)
514
516
  if isinstance(child, ast.Name) and isinstance(child.ctx, (ast.Store, ast.Del))
515
517
  }
516
518
 
@@ -521,7 +523,7 @@ def _is_unguarded_irrefutable(case: ast.match_case) -> bool:
521
523
 
522
524
  def _pattern_names(pattern: ast.pattern) -> set[str]:
523
525
  names: set[str] = set()
524
- for node in ast.walk(pattern):
526
+ for node in walk_ast(pattern):
525
527
  match node:
526
528
  case ast.MatchAs(name=name) | ast.MatchStar(name=name) if name is not None:
527
529
  names.add(name)
@@ -535,7 +537,7 @@ def _pattern_names(pattern: ast.pattern) -> set[str]:
535
537
  def _function_bound_names(node: ast.FunctionDef | ast.AsyncFunctionDef) -> set[str]:
536
538
  names = _argument_names(node.args)
537
539
  for statement in node.body:
538
- for child in ast.walk(statement):
540
+ for child in walk_ast(statement):
539
541
  names.update(_names_bound_by_node(child))
540
542
  return names
541
543
 
@@ -572,7 +574,7 @@ def _function_external_names(node: ast.FunctionDef | ast.AsyncFunctionDef) -> se
572
574
  return {
573
575
  name
574
576
  for statement in node.body
575
- for child in ast.walk(statement)
577
+ for child in walk_ast(statement)
576
578
  if isinstance(child, (ast.Global, ast.Nonlocal))
577
579
  for name in child.names
578
580
  }
@@ -6,6 +6,8 @@ from dataclasses import dataclass
6
6
  from types import MappingProxyType
7
7
  from typing import Self
8
8
 
9
+ from sarj_python_lint.rules._ast_index import walk as walk_ast
10
+
9
11
 
10
12
  @dataclass(frozen=True, slots=True)
11
13
  class _ImportTarget:
@@ -77,7 +79,7 @@ class ImportIndex:
77
79
 
78
80
  def _non_import_bindings(tree: ast.Module) -> set[str]:
79
81
  names: set[str] = set()
80
- for node in ast.walk(tree):
82
+ for node in walk_ast(tree):
81
83
  match node:
82
84
  case ast.Name(id=name, ctx=(ast.Store() | ast.Del())) | ast.arg(arg=name):
83
85
  names.add(name)
@@ -10,6 +10,7 @@ import re
10
10
  from types import MappingProxyType
11
11
  from typing import TYPE_CHECKING, final
12
12
 
13
+ from sarj_python_lint.rules._ast_index import walk as walk_ast
13
14
  from sarj_python_lint.rules._first_party import FirstPartyFacts, project_root
14
15
 
15
16
 
@@ -139,7 +140,7 @@ class ProjectIndexSet:
139
140
  return self._classes.get(symbol) if symbol is not None else None
140
141
 
141
142
  def annotation_contains_enum(self, unit: SourceUnit, annotation: ast.expr) -> bool:
142
- for member in ast.walk(annotation):
143
+ for member in walk_ast(annotation):
143
144
  if not isinstance(member, (ast.Name, ast.Attribute)):
144
145
  continue
145
146
  symbol = self.resolve(unit, member)
@@ -198,7 +199,7 @@ class ProjectIndexSet:
198
199
  and name in candidate.source
199
200
  and any(
200
201
  _is_mock_spec_for(candidate, call, target)
201
- for call in ast.walk(candidate.tree)
202
+ for call in walk_ast(candidate.tree)
202
203
  if isinstance(call, ast.Call)
203
204
  )
204
205
  )
@@ -614,7 +615,7 @@ def _annotation_contains_symbol(unit: SourceUnit, annotation: ast.expr | None, t
614
615
  annotation = ast.parse(annotation.value, mode="eval").body
615
616
  return any(
616
617
  _resolve(unit, candidate) == target
617
- for candidate in ast.walk(annotation)
618
+ for candidate in walk_ast(annotation)
618
619
  if isinstance(candidate, (ast.Name, ast.Attribute))
619
620
  )
620
621