sarj-python-lint 0.88.0__tar.gz → 1.1.1__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.1}/.gitignore +1 -1
  2. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/PKG-INFO +2 -1
  3. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/pyproject.toml +5 -2
  4. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/__main__.py +25 -17
  5. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/_analysis_session.py +6 -1
  6. sarj_python_lint-1.1.1/src/sarj_python_lint/_file_context.py +73 -0
  7. sarj_python_lint-1.1.1/src/sarj_python_lint/benchmark.py +118 -0
  8. sarj_python_lint-1.1.1/src/sarj_python_lint/rule_base.py +158 -0
  9. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_ast_index.py +22 -12
  10. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_fastapi.py +4 -3
  11. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_fixed_record.py +11 -9
  12. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_imports.py +3 -1
  13. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_project_index.py +4 -3
  14. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_prose_budget.py +3 -3
  15. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +65 -44
  17. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +8 -7
  19. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +8 -7
  20. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/excessive_commentary.py +10 -7
  21. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +7 -5
  22. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +26 -18
  23. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +12 -12
  24. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +8 -9
  25. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +12 -8
  26. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/mock_without_spec.py +29 -13
  27. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +17 -19
  28. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/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.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +23 -12
  31. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_comment_cruft.py +6 -3
  32. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +6 -5
  34. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/src/sarj_python_lint/rules/no_delete_statement.py +8 -8
  36. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_dunder_all.py +6 -6
  37. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +7 -7
  39. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +7 -5
  40. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/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.1}/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.1}/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.1}/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.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +8 -12
  46. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +13 -15
  48. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_offset_pagination.py +10 -10
  49. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/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.1}/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.1}/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.1}/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.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +9 -8
  55. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +7 -5
  57. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +8 -6
  58. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_restated_comment.py +14 -14
  59. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_secret_in_log.py +6 -5
  60. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_select_star.py +17 -14
  61. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/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.1}/src/sarj_python_lint/rules/no_tautological_expect.py +13 -12
  64. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +14 -11
  65. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +23 -18
  66. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +45 -25
  67. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +17 -14
  68. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +5 -5
  69. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +16 -13
  70. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/src/sarj_python_lint/rules/over_mocked_test.py +7 -5
  72. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/phase_label_comment.py +6 -5
  73. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_class_row.py +16 -9
  74. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +23 -18
  75. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +23 -20
  76. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +18 -16
  77. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +6 -6
  78. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/src/sarj_python_lint/rules/prefer_library_fake.py +7 -5
  80. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +17 -13
  81. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +30 -17
  82. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +10 -9
  83. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +17 -16
  84. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +25 -22
  85. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/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.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +15 -11
  88. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +17 -13
  89. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +8 -8
  90. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +9 -7
  91. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +21 -15
  92. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +22 -19
  93. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +14 -10
  94. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_str_enum.py +46 -27
  95. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +30 -20
  96. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +16 -12
  98. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +19 -13
  99. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +14 -10
  100. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +10 -7
  101. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +12 -12
  103. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/redundant_docstring.py +17 -13
  104. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +14 -9
  105. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +9 -7
  107. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/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.1}/src/sarj_python_lint/rules/require_port_for_service.py +31 -20
  110. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/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.1}/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.1}/src/sarj_python_lint/rules/restated_test_docstring.py +20 -15
  114. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/stepdown.py +7 -5
  115. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +19 -18
  117. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +7 -8
  118. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/trailing_value_narration.py +12 -10
  119. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/typed_error_reasons.py +11 -8
  120. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +7 -5
  121. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/unused_mock_setup.py +11 -10
  122. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/LICENSE +0 -0
  125. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/README.md +0 -0
  126. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/__init__.py +0 -0
  127. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/_filesystem.py +0 -0
  128. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/_python_target.py +0 -0
  129. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  130. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/_secret_names.py +0 -0
  131. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/_version.py +0 -0
  132. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/json_boundary.py +0 -0
  133. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/py.typed +0 -0
  134. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/ratchet.py +0 -0
  135. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/__init__.py +0 -0
  136. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  137. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  138. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_comments.py +0 -0
  139. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  140. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
  141. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_logging.py +0 -0
  142. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_paths.py +0 -0
  143. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
  144. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_registry.py +0 -0
  145. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_sql.py +0 -0
  146. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  147. {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1
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.1"
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 }
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import ast
4
4
  from collections import Counter
5
5
  from dataclasses import dataclass
6
+ from fnmatch import fnmatch
6
7
  import json
7
8
  from pathlib import Path
8
9
  import sys
@@ -13,6 +14,7 @@ import typer
13
14
 
14
15
  from sarj_python_lint import __version__
15
16
  from sarj_python_lint._analysis_session import AnalysisSession
17
+ from sarj_python_lint._file_context import PythonFileContext
16
18
  from sarj_python_lint._filesystem import atomic_write_text
17
19
  from sarj_python_lint.json_boundary import is_object_mapping, parse_json
18
20
  from sarj_python_lint.rule_base import Diagnostic, ProjectRule, Rule, Severity, is_suppressed
@@ -47,7 +49,7 @@ SKIP_DIR_NAMES = frozenset(
47
49
  _MAX_FILE_BYTES = 500_000
48
50
 
49
51
 
50
- def _expand_paths(paths: list[Path]) -> list[Path]:
52
+ def expand_paths(paths: list[Path]) -> list[Path]:
51
53
  out: list[Path] = []
52
54
  for p in paths:
53
55
  if not p.exists():
@@ -65,19 +67,21 @@ def _expand_paths(paths: list[Path]) -> list[Path]:
65
67
 
66
68
 
67
69
  def _python_files(p: Path) -> list[Path]:
70
+ if not SKIP_DIR_NAMES.isdisjoint(p.parts):
71
+ return []
68
72
  out: list[Path] = []
69
- for child in p.rglob("*.py"):
70
- if not child.is_file():
71
- continue
72
- if any(part in SKIP_DIR_NAMES for part in child.parts):
73
- continue
74
- try:
75
- if child.stat().st_size > _MAX_FILE_BYTES:
73
+ for directory, directories, filenames in p.walk():
74
+ directories[:] = [name for name in directories if name not in SKIP_DIR_NAMES]
75
+ for name in filenames:
76
+ if not fnmatch(name, "*.py"):
76
77
  continue
77
- except OSError:
78
- continue
79
- out.append(child)
80
- return out
78
+ child = directory / name
79
+ try:
80
+ if child.is_file() and child.stat().st_size <= _MAX_FILE_BYTES:
81
+ out.append(child)
82
+ except OSError:
83
+ continue
84
+ return sorted(out)
81
85
 
82
86
 
83
87
  def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
@@ -91,7 +95,7 @@ def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
91
95
  for rule in rules:
92
96
  rule.prepare_session(session)
93
97
  clear_path_caches()
94
- expanded = _expand_paths(paths)
98
+ expanded = expand_paths(paths)
95
99
  loaded: dict[Path, str] = {}
96
100
  for path in expanded:
97
101
  try:
@@ -101,17 +105,21 @@ def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
101
105
  project_rules = [rule for rule in rules if isinstance(rule, ProjectRule)]
102
106
  if project_rules:
103
107
  indexes = ProjectIndexSet.build(expanded, loaded, facts=session.first_party)
108
+ session.project = indexes
104
109
  for rule in project_rules:
105
110
  rule.prepare(indexes)
106
111
  diags: list[Diagnostic] = []
107
112
  for p, source in loaded.items():
108
- diags.extend(_check_source(rules, p, source))
113
+ diags.extend(check_source(rules, p, source, session=session))
109
114
  return diags
110
115
 
111
116
 
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)]
117
+ def check_source(
118
+ rules: list[Rule], p: Path, source: str, *, session: AnalysisSession | None = None
119
+ ) -> list[Diagnostic]:
120
+ context = PythonFileContext(p, source, session)
121
+ source_lines = context.source_lines
122
+ raw = [diagnostic for rule in rules for diagnostic in rule.check_context(context)]
115
123
  return deduplicate_diagnostics(
116
124
  [
117
125
  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,73 @@
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 collections.abc import Mapping
16
+ from pathlib import Path
17
+
18
+
19
+ @final
20
+ class PythonFileContext:
21
+ def __init__(self, path: Path, source: str, session: AnalysisSession | None = None) -> None:
22
+ self.path = path
23
+ self.source = source
24
+ self.session = session if session is not None else AnalysisSession()
25
+
26
+ @cached_property
27
+ def tree(self) -> ast.Module | None:
28
+ if self.session.project is not None:
29
+ unit = self.session.project.unit(self.path)
30
+ if unit is not None and unit.source == self.source:
31
+ return unit.tree
32
+ try:
33
+ return ast.parse(self.source, filename=str(self.path))
34
+ except SyntaxError:
35
+ return None
36
+
37
+ @cached_property
38
+ def source_lines(self) -> list[str]:
39
+ return self.source.splitlines()
40
+
41
+ @cached_property
42
+ def generated(self) -> bool:
43
+ return is_generated(self.path, self.source)
44
+
45
+ @cached_property
46
+ def node_index(self) -> NodeIndex:
47
+ return NodeIndex(self._valid_tree())
48
+
49
+ def nodes[NodeT: ast.AST](self, *types: type[NodeT]) -> list[NodeT]:
50
+ return [node for node in self.node_index.query(types) if isinstance(node, types)]
51
+
52
+ @property
53
+ def parents(self) -> Mapping[ast.AST, ast.AST]:
54
+ return self.node_index.parents
55
+
56
+ @cached_property
57
+ def imports(self) -> ImportIndex:
58
+ return ImportIndex.from_tree(self._valid_tree())
59
+
60
+ @cached_property
61
+ def module_imports(self) -> ImportIndex:
62
+ return ImportIndex.from_tree(self._valid_tree(), module_scope_only=True)
63
+
64
+ @cached_property
65
+ def fastapi(self) -> FastapiIndex:
66
+ return FastapiIndex(self._valid_tree(), path=self.path)
67
+
68
+ def _valid_tree(self) -> ast.Module:
69
+ tree = self.tree
70
+ if tree is None:
71
+ msg = "syntax-dependent facts require a successfully parsed source file"
72
+ raise ValueError(msg)
73
+ 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
@@ -3,11 +3,12 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import ast
6
+ from types import MappingProxyType
6
7
  from typing import TYPE_CHECKING, TypeIs, final
7
8
 
8
9
 
9
10
  if TYPE_CHECKING:
10
- from collections.abc import Iterator
11
+ from collections.abc import Iterator, Mapping
11
12
 
12
13
 
13
14
  _AST = ast.AST
@@ -28,7 +29,10 @@ def children(node: ast.AST) -> list[ast.AST]:
28
29
  return out
29
30
 
30
31
 
31
- def walk(node: ast.AST) -> Iterator[ast.AST]:
32
+ def walk(node: ast.AST, *, index: NodeIndex | None = None) -> Iterator[ast.AST]:
33
+ if index is not None and index.tree is node:
34
+ yield from index.query((ast.AST,))
35
+ return
32
36
  queue: list[ast.AST] = [node]
33
37
  i = 0
34
38
  while i < len(queue):
@@ -39,10 +43,11 @@ def walk(node: ast.AST) -> Iterator[ast.AST]:
39
43
 
40
44
 
41
45
  @final
42
- class _NodeIndex:
43
- __slots__ = ("_buckets", "_flat", "_queries")
46
+ class NodeIndex:
47
+ __slots__ = ("_buckets", "_flat", "_parents", "_queries", "tree")
44
48
 
45
49
  def __init__(self, tree: ast.AST) -> None:
50
+ self.tree = tree
46
51
  buckets: dict[type[ast.AST], list[ast.AST]] = {}
47
52
  flat: list[ast.AST] = [tree]
48
53
  i = 0
@@ -58,8 +63,15 @@ class _NodeIndex:
58
63
  flat += children(current)
59
64
  self._buckets: dict[type[ast.AST], list[ast.AST]] = buckets
60
65
  self._flat: list[ast.AST] = flat
66
+ self._parents: Mapping[ast.AST, ast.AST] | None = None
61
67
  self._queries: dict[tuple[type[ast.AST], ...], list[ast.AST]] = {}
62
68
 
69
+ @property
70
+ def parents(self) -> Mapping[ast.AST, ast.AST]:
71
+ if self._parents is None:
72
+ self._parents = MappingProxyType({child: parent for parent in self._flat for child in children(parent)})
73
+ return self._parents
74
+
63
75
  def query(self, types: tuple[type[ast.AST], ...]) -> list[ast.AST]:
64
76
  hit = self._queries.get(types)
65
77
  if hit is not None:
@@ -80,13 +92,11 @@ class _NodeIndex:
80
92
  return result
81
93
 
82
94
 
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.
95
+ def nodes[NodeT: ast.AST](tree: ast.AST, *types: type[NodeT], index: NodeIndex | None = None) -> list[NodeT]:
96
+ selected = index if index is not None and index.tree is tree else NodeIndex(tree)
97
+ return [node for node in selected.query(types) if isinstance(node, types)]
85
98
 
86
99
 
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)]
100
+ def parent_map(tree: ast.AST, *, index: NodeIndex | None = None) -> Mapping[ast.AST, ast.AST]:
101
+ selected = index if index is not None and index.tree is tree else NodeIndex(tree)
102
+ return selected.parents
@@ -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