sarj-python-lint 1.3.3__tar.gz → 1.4.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 (155) hide show
  1. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/pyproject.toml +1 -1
  3. sarj_python_lint-1.4.0/src/sarj_python_lint/rules/_dict_key_views.py +105 -0
  4. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/_registry.py +6 -0
  5. sarj_python_lint-1.4.0/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +233 -0
  6. sarj_python_lint-1.4.0/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +298 -0
  7. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +19 -4
  8. sarj_python_lint-1.4.0/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +255 -0
  9. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/.gitignore +0 -0
  10. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/LICENSE +0 -0
  11. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/README.md +0 -0
  12. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/__init__.py +0 -0
  13. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/__main__.py +0 -0
  14. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  15. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/_file_context.py +0 -0
  16. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/_filesystem.py +0 -0
  17. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/_python_target.py +0 -0
  18. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  19. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/_secret_names.py +0 -0
  20. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/_version.py +0 -0
  21. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/benchmark.py +0 -0
  22. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/json_boundary.py +0 -0
  23. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/py.typed +0 -0
  24. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/ratchet.py +0 -0
  25. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rule_base.py +0 -0
  26. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  27. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  28. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  29. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  30. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  31. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  32. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  33. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  34. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  35. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  36. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  37. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  38. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  39. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  40. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  41. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  42. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  43. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  44. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  45. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  46. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  47. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  48. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  49. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  50. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  51. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  52. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  53. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  54. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  55. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  56. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  57. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  58. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  59. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  60. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  61. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  62. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  63. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  64. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  65. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  66. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  67. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  68. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  69. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  70. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  71. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  72. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  73. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  74. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  75. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  76. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  77. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  78. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  79. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  80. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  81. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  82. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  83. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  84. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  85. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  86. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  87. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  88. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  89. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  90. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  91. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  92. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  93. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  94. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  95. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  96. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  97. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  98. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  99. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  100. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  101. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  102. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  103. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  104. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  105. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  106. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  107. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  108. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  109. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  110. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  111. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  112. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  113. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  114. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  115. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  116. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  117. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  118. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  119. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  120. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  121. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  122. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  123. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  124. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  125. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  126. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  127. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  128. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  129. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  130. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  131. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  132. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  133. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  134. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  135. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  136. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  137. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  138. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  139. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  140. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  141. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  142. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  143. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  144. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  145. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  146. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  147. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  148. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  149. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  150. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  151. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  152. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  153. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  154. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  155. {sarj_python_lint-1.3.3 → sarj_python_lint-1.4.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.3.3
3
+ Version: 1.4.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 = "1.3.3"
3
+ version = "1.4.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" }]
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from typing import TYPE_CHECKING
5
+
6
+ from sarj_python_lint.rules._ast_index import walk
7
+
8
+
9
+ if TYPE_CHECKING:
10
+ from collections.abc import Mapping
11
+
12
+
13
+ _SCOPES = (ast.Module, ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
14
+ _NESTED = (ast.Lambda, ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)
15
+
16
+
17
+ def proven_dict_key_views(tree: ast.Module, shadowed: frozenset[str]) -> set[ast.Call]:
18
+ parents = {child: parent for parent in walk(tree) for child in ast.iter_child_nodes(parent)}
19
+ result: set[ast.Call] = set()
20
+ scopes: dict[ast.AST, dict[str, ast.AST]] = {}
21
+ for node in walk(tree):
22
+ if not (
23
+ isinstance(node, ast.Call)
24
+ and isinstance(node.func, ast.Attribute)
25
+ and node.func.attr == "keys"
26
+ and not node.args
27
+ and not node.keywords
28
+ ):
29
+ continue
30
+ receiver = node.func.value
31
+ if _literal_dict(receiver, shadowed) or (
32
+ isinstance(receiver, ast.Name) and _local_dict(receiver, parents, shadowed, scopes)
33
+ ):
34
+ result.add(node)
35
+ return result
36
+
37
+
38
+ def _literal_dict(node: ast.expr, shadowed: frozenset[str]) -> bool:
39
+ return isinstance(node, ast.Dict | ast.DictComp) or (
40
+ isinstance(node, ast.Call)
41
+ and isinstance(node.func, ast.Name)
42
+ and node.func.id == "dict"
43
+ and "dict" not in shadowed
44
+ )
45
+
46
+
47
+ def _local_dict(
48
+ node: ast.Name,
49
+ parents: Mapping[ast.AST, ast.AST],
50
+ shadowed: frozenset[str],
51
+ scopes: dict[ast.AST, dict[str, ast.AST]],
52
+ ) -> bool:
53
+ ancestor: ast.AST = node
54
+ while not isinstance(ancestor, _SCOPES):
55
+ if isinstance(ancestor, _NESTED):
56
+ return False
57
+ ancestor = parents[ancestor]
58
+ if isinstance(ancestor, ast.ClassDef):
59
+ return False
60
+ if ancestor not in scopes:
61
+ scopes[ancestor] = _unique_bindings(ancestor)
62
+ binding = scopes[ancestor].get(node.id)
63
+ if binding is None:
64
+ return False
65
+ statement = parents.get(binding)
66
+ if not isinstance(statement, ast.Assign | ast.AnnAssign) or parents.get(statement) is not ancestor:
67
+ return False
68
+ if isinstance(statement, ast.Assign) and len(statement.targets) != 1:
69
+ return False
70
+ return statement.lineno < node.lineno and statement.value is not None and _literal_dict(statement.value, shadowed)
71
+
72
+
73
+ def _unique_bindings(scope: ast.AST) -> dict[str, ast.AST]:
74
+ bindings: dict[str, ast.AST] = {}
75
+ repeated: set[str] = set()
76
+ for node in walk(scope):
77
+ for name in _bound_names(node):
78
+ if name == "*":
79
+ return {}
80
+ if name in bindings:
81
+ repeated.add(name)
82
+ bindings[name] = node
83
+ return {name: node for name, node in bindings.items() if name not in repeated}
84
+
85
+
86
+ def _bound_names(node: ast.AST) -> tuple[str, ...]:
87
+ match node:
88
+ case ast.Name(id=name, ctx=ast.Store() | ast.Del()) | ast.arg(arg=name):
89
+ return (name,)
90
+ case ast.alias():
91
+ return (node.asname or node.name.split(".")[0],)
92
+ case (
93
+ ast.ExceptHandler(name=str() as name)
94
+ | ast.MatchAs(name=str() as name)
95
+ | ast.MatchStar(name=str() as name)
96
+ | ast.MatchMapping(rest=str() as name)
97
+ | ast.FunctionDef(name=name)
98
+ | ast.AsyncFunctionDef(name=name)
99
+ | ast.ClassDef(name=name)
100
+ ):
101
+ return (name,)
102
+ case ast.Global(names=names) | ast.Nonlocal(names=names):
103
+ return tuple(names)
104
+ case _:
105
+ return ()
@@ -115,6 +115,7 @@ from sarj_python_lint.rules.prefer_module_level_constant import (
115
115
  from sarj_python_lint.rules.prefer_monkeypatch_for_process_state_in_test import (
116
116
  PreferMonkeypatchForProcessStateInTest,
117
117
  )
118
+ from sarj_python_lint.rules.prefer_monotonic_for_elapsed_time import PreferMonotonicForElapsedTime
118
119
  from sarj_python_lint.rules.prefer_namedtuple_over_tuple_return import (
119
120
  PreferNamedtupleOverTupleReturn,
120
121
  )
@@ -124,6 +125,7 @@ from sarj_python_lint.rules.prefer_non_nullable_collection import (
124
125
  )
125
126
  from sarj_python_lint.rules.prefer_or_pattern import PreferOrPattern
126
127
  from sarj_python_lint.rules.prefer_pydantic_json_value import PreferPydanticJsonValue
128
+ from sarj_python_lint.rules.prefer_regex_fullmatch import PreferRegexFullmatch
127
129
  from sarj_python_lint.rules.prefer_self_documenting_constant import (
128
130
  PreferSelfDocumentingConstant,
129
131
  )
@@ -162,6 +164,7 @@ from sarj_python_lint.rules.require_pydantic_ordinal_lower_bound import (
162
164
  RequirePydanticOrdinalLowerBound,
163
165
  )
164
166
  from sarj_python_lint.rules.restated_test_docstring import RestatedTestDocstring
167
+ from sarj_python_lint.rules.shared_mutable_pydantic_factory import SharedMutablePydanticFactory
165
168
  from sarj_python_lint.rules.stepdown import Stepdown
166
169
  from sarj_python_lint.rules.store_get_delegates_to_bulk_read import StoreGetDelegatesToBulkRead
167
170
  from sarj_python_lint.rules.store_insert_requires_on_conflict import (
@@ -291,6 +294,9 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
291
294
  RequireNoDecodeForSplittingSettingsField.id: RequireNoDecodeForSplittingSettingsField,
292
295
  NoVagueSuppressionDescription.id: NoVagueSuppressionDescription,
293
296
  NoWholeRequestResponsePayloadInLog.id: NoWholeRequestResponsePayloadInLog,
297
+ SharedMutablePydanticFactory.id: SharedMutablePydanticFactory,
298
+ PreferRegexFullmatch.id: PreferRegexFullmatch,
299
+ PreferMonotonicForElapsedTime.id: PreferMonotonicForElapsedTime,
294
300
  TypedErrorReasons.id: TypedErrorReasons,
295
301
  }
296
302
  )
@@ -0,0 +1,233 @@
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
+ Severity,
17
+ is_suppressed,
18
+ )
19
+
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Iterator
23
+
24
+ from sarj_python_lint._file_context import PythonFileContext
25
+
26
+
27
+ type _Function = ast.FunctionDef | ast.AsyncFunctionDef
28
+ _SCOPES = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)
29
+ _COMPREHENSIONS = (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)
30
+ _REFLECTIVE_CALLS = frozenset({"locals", "globals", "vars", "eval", "exec", "setattr", "delattr"})
31
+
32
+
33
+ @final
34
+ class PreferMonotonicForElapsedTime(Rule):
35
+ id = "prefer-monotonic-for-elapsed-time"
36
+ code = "SARJ461"
37
+ documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
38
+ default_level=Severity.WARNING,
39
+ summary="Use a monotonic clock for a local wall-clock value used only to measure elapsed time.",
40
+ rationale=(
41
+ "System-clock corrections can make wall-clock differences negative or unexpectedly large. "
42
+ "A monotonic clock measures elapsed intervals without these jumps."
43
+ ),
44
+ remediation=(
45
+ "Use time.monotonic() or time.perf_counter() for both the start reading and its elapsed-time readings. "
46
+ "Keep time.time() when measuring absolute timestamps or deliberate wall-clock adjustments."
47
+ ),
48
+ category=RuleCategory.CORRECTNESS,
49
+ autofix=AutofixPolicy.NONE,
50
+ limitations=(
51
+ "Only an unconditional, single-assignment function-local start reading whose every read is the right operand of time.time() - start is reported.",
52
+ "Only unshadowed module-level clock imports are resolved. Reassignment, additional timestamp uses, escapes, nested-scope reads, comprehensions, reflection, and mixed-clock expressions are excluded.",
53
+ "Instance fields, process or persisted deadlines, indirect end-clock readings, and generated files are not inferred. No automatic rewrite is provided.",
54
+ ),
55
+ examples=(
56
+ RuleExample(
57
+ example_id="wall-clock-elapsed",
58
+ title="A local wall clock measures duration",
59
+ outcome=ExampleOutcome.MATCH,
60
+ files=(
61
+ ExampleFile.python(
62
+ "app/metrics.py",
63
+ "import time\n\ndef measure():\n started = time.time()\n work()\n return time.time() - started\n",
64
+ ),
65
+ ),
66
+ focus_path=PurePosixPath("app/metrics.py"),
67
+ expected_count=1,
68
+ public=True,
69
+ ),
70
+ RuleExample(
71
+ example_id="monotonic-elapsed",
72
+ title="A monotonic clock measures duration",
73
+ outcome=ExampleOutcome.NO_MATCH,
74
+ files=(
75
+ ExampleFile.python(
76
+ "app/metrics.py",
77
+ "import time\n\ndef measure():\n started = time.monotonic()\n work()\n return time.monotonic() - started\n",
78
+ ),
79
+ ),
80
+ focus_path=PurePosixPath("app/metrics.py"),
81
+ expected_count=0,
82
+ public=True,
83
+ ),
84
+ ),
85
+ )
86
+ description = documentation.summary
87
+
88
+ @override
89
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
90
+ if "time" not in context.source or context.generated or context.tree is None or _clock_is_mutated(context):
91
+ return []
92
+ findings: list[Diagnostic] = []
93
+ for function in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef):
94
+ for start in _elapsed_starts(context, function):
95
+ if is_suppressed(context.source_lines, start.lineno, self.code):
96
+ continue
97
+ findings.append(
98
+ Diagnostic(
99
+ path=context.path,
100
+ line=start.lineno,
101
+ col=start.col_offset + 1,
102
+ code=self.code,
103
+ severity=Severity.WARNING,
104
+ message="this wall-clock reading is used only for elapsed time; use a monotonic clock for both the start and elapsed readings",
105
+ )
106
+ )
107
+ return sorted(findings, key=lambda finding: (finding.line, finding.col))
108
+
109
+
110
+ def _elapsed_starts(context: PythonFileContext, function: _Function) -> Iterator[ast.Call]:
111
+ nodes = tuple(ast.walk(function))
112
+ if any(_reflective_call(node) for node in nodes):
113
+ return
114
+ for statement in function.body:
115
+ candidate = _start_assignment(statement)
116
+ if candidate is None:
117
+ continue
118
+ target, call = candidate
119
+ if not _wall_clock(context, call) or any(_rebinds(node, target) for node in nodes):
120
+ continue
121
+ reads = _reads_of(nodes, target.id)
122
+ if reads and all(_elapsed_read(context, function, call, read) for read in reads):
123
+ yield call
124
+
125
+
126
+ def _start_assignment(statement: ast.stmt) -> tuple[ast.Name, ast.Call] | None:
127
+ match statement:
128
+ case (
129
+ ast.Assign(targets=[ast.Name() as target], value=ast.Call() as call)
130
+ | ast.AnnAssign(target=ast.Name() as target, value=ast.Call() as call)
131
+ ):
132
+ return target, call
133
+ case _:
134
+ return None
135
+
136
+
137
+ def _reads_of(nodes: tuple[ast.AST, ...], name: str) -> list[ast.Name]:
138
+ return [node for node in nodes if isinstance(node, ast.Name) and node.id == name and isinstance(node.ctx, ast.Load)]
139
+
140
+
141
+ def _wall_clock(context: PythonFileContext, call: ast.expr) -> bool:
142
+ return (
143
+ isinstance(call, ast.Call)
144
+ and not call.args
145
+ and not call.keywords
146
+ and context.imports.resolved_qualified_name(call.func) == "time.time"
147
+ )
148
+
149
+
150
+ def _elapsed_read(context: PythonFileContext, function: _Function, start: ast.Call, read: ast.Name) -> bool:
151
+ if (read.lineno, read.col_offset) <= (start.lineno, start.col_offset):
152
+ return False
153
+ parent = context.parents.get(read)
154
+ if not isinstance(parent, ast.BinOp) or not isinstance(parent.op, ast.Sub) or parent.right is not read:
155
+ return False
156
+ if not _wall_clock(context, parent.left):
157
+ return False
158
+ owner = context.parents.get(parent)
159
+ while owner is not None and owner is not function:
160
+ if isinstance(owner, (*_SCOPES, *_COMPREHENSIONS)):
161
+ return False
162
+ owner = context.parents.get(owner)
163
+ return owner is function
164
+
165
+
166
+ def _rebinds(node: ast.AST, target: ast.Name) -> bool:
167
+ match node:
168
+ case ast.Name(id=name, ctx=ast.Store() | ast.Del()):
169
+ return node is not target and name == target.id
170
+ case ast.arg(arg=name) | ast.FunctionDef(name=name) | ast.AsyncFunctionDef(name=name) | ast.ClassDef(name=name):
171
+ return name == target.id
172
+ case ast.alias(name=name, asname=alias):
173
+ return (alias or name.partition(".")[0]) == target.id
174
+ case ast.Global(names=names) | ast.Nonlocal(names=names):
175
+ return target.id in names
176
+ case (
177
+ ast.ExceptHandler(name=name)
178
+ | ast.MatchAs(name=name)
179
+ | ast.MatchStar(name=name)
180
+ | ast.MatchMapping(rest=name)
181
+ ):
182
+ return name == target.id
183
+ case _:
184
+ return False
185
+
186
+
187
+ def _reflective_call(node: ast.AST) -> bool:
188
+ if not isinstance(node, ast.Call):
189
+ return False
190
+ return (isinstance(node.func, ast.Name) and node.func.id in _REFLECTIVE_CALLS) or (
191
+ isinstance(node.func, ast.Attribute) and node.func.attr in _REFLECTIVE_CALLS
192
+ )
193
+
194
+
195
+ def _clock_is_mutated(context: PythonFileContext) -> bool:
196
+ if any(
197
+ isinstance(node.ctx, (ast.Store, ast.Del)) and context.imports.resolved_qualified_name(node) == "time.time"
198
+ for node in context.nodes(ast.Attribute)
199
+ ):
200
+ return True
201
+ clocks = _clock_imports(context)
202
+ if context.tree is None:
203
+ return False
204
+ for node in ast.walk(context.tree):
205
+ if isinstance(node, ast.alias):
206
+ if node.name == "*":
207
+ return True
208
+ statement = context.parents.get(node)
209
+ if _is_direct_clock_import(context, statement, node):
210
+ continue
211
+ if any(_rebinds(node, clock) for clock in clocks):
212
+ return True
213
+ return False
214
+
215
+
216
+ def _clock_imports(context: PythonFileContext) -> list[ast.Name]:
217
+ return [
218
+ ast.Name(id=name, ctx=ast.Load())
219
+ for name in context.imports.bindings
220
+ if context.imports.resolved_qualified_name(ast.Name(id=name, ctx=ast.Load())) == "time.time"
221
+ or context.imports.resolved_qualified_name(
222
+ ast.Attribute(value=ast.Name(id=name, ctx=ast.Load()), attr="time", ctx=ast.Load())
223
+ )
224
+ == "time.time"
225
+ ]
226
+
227
+
228
+ def _is_direct_clock_import(context: PythonFileContext, statement: ast.AST | None, alias: ast.alias) -> bool:
229
+ if statement is None or context.parents.get(statement) is not context.tree or alias.name != "time":
230
+ return False
231
+ return isinstance(statement, ast.Import) or (
232
+ isinstance(statement, ast.ImportFrom) and statement.module == "time" and statement.level == 0
233
+ )
@@ -0,0 +1,298 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from collections import Counter
5
+ from pathlib import PurePosixPath
6
+ import re
7
+ from typing import TYPE_CHECKING, ClassVar, override
8
+
9
+ from sarj_python_lint.rule_base import (
10
+ AutofixPolicy,
11
+ Diagnostic,
12
+ ExampleFile,
13
+ ExampleOutcome,
14
+ Rule,
15
+ RuleCategory,
16
+ RuleDocumentation,
17
+ RuleExample,
18
+ Severity,
19
+ is_suppressed,
20
+ )
21
+ from sarj_python_lint.rules._paths import is_test_path
22
+
23
+
24
+ if TYPE_CHECKING:
25
+ from sarj_python_lint._file_context import PythonFileContext
26
+
27
+
28
+ _RE = frozenset({"re"})
29
+ _SCOPES = (ast.Module, ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)
30
+ _BAD = "import re\n\ndef validate(value):\n if not re.match(r'^[a-z]+$', value):\n raise ValueError('invalid value')\n"
31
+ _GOOD = _BAD.replace("re.match(r'^[a-z]+$'", "re.fullmatch(r'[a-z]+'")
32
+
33
+
34
+ class PreferRegexFullmatch(Rule):
35
+ id: str = "prefer-regex-fullmatch"
36
+ code: str = "SARJ460"
37
+ documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
38
+ default_level=Severity.WARNING,
39
+ summary="An anchored regex match rejects invalid values without requiring a full-string match.",
40
+ rationale="The regex `$` anchor can match before a final newline, so an anchored match can accept a value that was not fully validated.",
41
+ remediation="Use `re.fullmatch` or the compiled pattern's `fullmatch` method for whole-value validation; review whether a terminal newline is intentionally allowed.",
42
+ category=RuleCategory.CORRECTNESS,
43
+ autofix=AutofixPolicy.NONE,
44
+ limitations=(
45
+ "Only direct negative match or `is None` conditions whose sole rejection statement raises are checked; extraction, search, compound conditions, and return-based validators are excluded.",
46
+ "Patterns must be valid string or bytes literals bounded by `^` or `\\A` and an unescaped outer `$`; top-level alternation, inline extension groups other than noncapturing groups, and flags other than literal zero are excluded.",
47
+ "Only imported standard-library re functions and unique, unconditional module or same-function compile bindings are resolved. Rebinding, import shadowing, escaped regex objects or modules, explicit match positions, tests, and generated files are excluded.",
48
+ "Binding checks are deliberately file-wide, so unrelated same-named locals can suppress findings. Dynamic monkeypatching and interprocedural changes are not inferred.",
49
+ ),
50
+ examples=(
51
+ RuleExample(
52
+ example_id="anchored-rejection",
53
+ title="An end anchor permits a final newline",
54
+ outcome=ExampleOutcome.MATCH,
55
+ files=(ExampleFile.python("app/validation.py", _BAD),),
56
+ focus_path=PurePosixPath("app/validation.py"),
57
+ expected_count=1,
58
+ public=True,
59
+ ),
60
+ RuleExample(
61
+ example_id="whole-value-rejection",
62
+ title="Validate the entire value",
63
+ outcome=ExampleOutcome.NO_MATCH,
64
+ files=(ExampleFile.python("app/validation.py", _GOOD),),
65
+ focus_path=PurePosixPath("app/validation.py"),
66
+ expected_count=0,
67
+ public=True,
68
+ ),
69
+ ),
70
+ )
71
+ description: str = documentation.summary
72
+
73
+ @override
74
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
75
+ if "match" not in context.source or context.tree is None or context.generated or is_test_path(context.path):
76
+ return []
77
+ bindings = _binding_counts(context)
78
+ mutated = {
79
+ root
80
+ for node in context.nodes(ast.Attribute)
81
+ if isinstance(node.ctx, (ast.Store, ast.Del)) and (root := _root_name(node)) is not None
82
+ }
83
+ mutated.update(
84
+ node.id for node in context.nodes(ast.Name) if isinstance(node.ctx, ast.Load) and _escapes(context, node)
85
+ )
86
+ compiled = _compiled_patterns(context, bindings, mutated)
87
+ findings: list[Diagnostic] = []
88
+ for guard in context.nodes(ast.If):
89
+ call = _rejection_call(guard)
90
+ if call is None or is_suppressed(context.source_lines, call.lineno, self.code):
91
+ continue
92
+ pattern = _match_pattern(context, call, bindings, mutated, compiled)
93
+ if pattern is None or not _whole_value_pattern(pattern):
94
+ continue
95
+ findings.append(
96
+ Diagnostic(
97
+ path=context.path,
98
+ line=call.lineno,
99
+ col=call.col_offset + 1,
100
+ code=self.code,
101
+ message="anchored match is used to reject invalid values, but `$` can accept a final newline; use fullmatch for whole-value validation and review newline handling.",
102
+ severity=Severity.WARNING,
103
+ )
104
+ )
105
+ return sorted(findings, key=lambda item: (item.line, item.col))
106
+
107
+
108
+ def _rejection_call(guard: ast.If) -> ast.Call | None:
109
+ if len(guard.body) != 1 or not isinstance(guard.body[0], ast.Raise):
110
+ return None
111
+ condition = guard.test
112
+ if isinstance(condition, ast.UnaryOp) and isinstance(condition.op, ast.Not):
113
+ return condition.operand if isinstance(condition.operand, ast.Call) else None
114
+ if not isinstance(condition, ast.Compare) or len(condition.ops) != 1:
115
+ return None
116
+ if (
117
+ isinstance(condition.ops[0], ast.Is)
118
+ and isinstance(condition.left, ast.Call)
119
+ and isinstance(condition.comparators[0], ast.Constant)
120
+ and condition.comparators[0].value is None
121
+ ):
122
+ return condition.left
123
+ return None
124
+
125
+
126
+ def _binding_counts(context: PythonFileContext) -> Counter[str]:
127
+ result: Counter[str] = Counter()
128
+ for node in context.nodes(ast.AST):
129
+ match node:
130
+ case (
131
+ ast.Name(id=name, ctx=(ast.Store() | ast.Del()))
132
+ | ast.arg(arg=name)
133
+ | ast.FunctionDef(name=name)
134
+ | ast.AsyncFunctionDef(name=name)
135
+ | ast.ClassDef(name=name)
136
+ ):
137
+ result[name] += 1
138
+ case ast.alias(name=name, asname=alias):
139
+ result[alias or name.partition(".")[0]] += 1
140
+ case (
141
+ ast.ExceptHandler(name=str(name))
142
+ | ast.MatchAs(name=str(name))
143
+ | ast.MatchStar(name=str(name))
144
+ | ast.MatchMapping(rest=str(name))
145
+ ):
146
+ result[name] += 1
147
+ case _:
148
+ pass
149
+ return result
150
+
151
+
152
+ def _root_name(node: ast.expr) -> str | None:
153
+ while isinstance(node, ast.Attribute):
154
+ node = node.value
155
+ return node.id if isinstance(node, ast.Name) else None
156
+
157
+
158
+ def _escapes(context: PythonFileContext, node: ast.Name) -> bool:
159
+ parent = context.parents.get(node)
160
+ if isinstance(parent, ast.Attribute) and parent.value is node:
161
+ return False
162
+ return not (isinstance(parent, ast.Call) and parent.func is node)
163
+
164
+
165
+ def _resolves(
166
+ context: PythonFileContext, node: ast.expr, symbol: str, bindings: Counter[str], mutated: set[str]
167
+ ) -> bool:
168
+ root = _root_name(node)
169
+ return (
170
+ root is not None
171
+ and bindings[root] == 1
172
+ and root not in mutated
173
+ and context.imports.resolves(node, sources=_RE, symbol=symbol)
174
+ )
175
+
176
+
177
+ def _literal_pattern(call: ast.Call, *, compile_call: bool) -> str | bytes | None:
178
+ names = ("pattern", "flags") if compile_call else ("pattern", "string", "flags")
179
+ if len(call.args) > len(names) or any(isinstance(argument, ast.Starred) for argument in call.args):
180
+ return None
181
+ arguments: dict[str, ast.expr] = dict(zip(names, call.args, strict=False))
182
+ for keyword in call.keywords:
183
+ if keyword.arg is None or keyword.arg not in names or keyword.arg in arguments:
184
+ return None
185
+ arguments[keyword.arg] = keyword.value
186
+ required = {"pattern"} if compile_call else {"pattern", "string"}
187
+ if not required <= arguments.keys():
188
+ return None
189
+ flags = arguments.get("flags")
190
+ if flags is not None and not (isinstance(flags, ast.Constant) and type(flags.value) is int and flags.value == 0):
191
+ return None
192
+ pattern = arguments["pattern"]
193
+ return pattern.value if isinstance(pattern, ast.Constant) and isinstance(pattern.value, (str, bytes)) else None
194
+
195
+
196
+ def _compiled_patterns(
197
+ context: PythonFileContext, bindings: Counter[str], mutated: set[str]
198
+ ) -> dict[str, tuple[ast.AST, ast.expr, str | bytes]]:
199
+ patterns: dict[str, tuple[ast.AST, ast.expr, str | bytes]] = {}
200
+ for node in context.nodes(ast.Assign, ast.AnnAssign):
201
+ if isinstance(node, ast.Assign) and len(node.targets) == 1:
202
+ target, value = node.targets[0], node.value
203
+ elif isinstance(node, ast.AnnAssign):
204
+ target, value = node.target, node.value
205
+ else:
206
+ continue
207
+ owner = context.parents.get(node)
208
+ if not isinstance(owner, (ast.Module, ast.FunctionDef, ast.AsyncFunctionDef)):
209
+ continue
210
+ if (
211
+ not isinstance(target, ast.Name)
212
+ or bindings[target.id] != 1
213
+ or target.id in mutated
214
+ or not isinstance(value, ast.Call)
215
+ or not _resolves(context, value.func, "compile", bindings, mutated)
216
+ ):
217
+ continue
218
+ pattern = _literal_pattern(value, compile_call=True)
219
+ if pattern is not None:
220
+ patterns[target.id] = owner, target, pattern
221
+ return patterns
222
+
223
+
224
+ def _match_pattern(
225
+ context: PythonFileContext,
226
+ call: ast.Call,
227
+ bindings: Counter[str],
228
+ mutated: set[str],
229
+ compiled: dict[str, tuple[ast.AST, ast.expr, str | bytes]],
230
+ ) -> str | bytes | None:
231
+ if _resolves(context, call.func, "match", bindings, mutated):
232
+ return _literal_pattern(call, compile_call=False)
233
+ if not isinstance(call.func, ast.Attribute) or call.func.attr != "match" or len(call.args) != 1 or call.keywords:
234
+ return None
235
+ receiver = call.func.value
236
+ if isinstance(receiver, ast.Call) and _resolves(context, receiver.func, "compile", bindings, mutated):
237
+ return _literal_pattern(receiver, compile_call=True)
238
+ if not isinstance(receiver, ast.Name) or receiver.id not in compiled:
239
+ return None
240
+ owner, target, pattern = compiled[receiver.id]
241
+ if target.lineno >= call.lineno or (not isinstance(owner, ast.Module) and owner is not _scope(context, call)):
242
+ return None
243
+ return pattern
244
+
245
+
246
+ def _scope(context: PythonFileContext, node: ast.AST) -> ast.AST | None:
247
+ parent = context.parents.get(node)
248
+ while parent is not None and not isinstance(parent, _SCOPES):
249
+ parent = context.parents.get(parent)
250
+ return parent
251
+
252
+
253
+ def _whole_value_pattern(pattern: str | bytes) -> bool:
254
+ text = pattern.decode("latin1") if isinstance(pattern, bytes) else pattern
255
+ if not text.startswith(("^", r"\A")) or not text.endswith("$"):
256
+ return False
257
+ try:
258
+ re.compile(pattern)
259
+ except re.PatternError, OverflowError, RecursionError:
260
+ return False
261
+ depth = 0
262
+ index = 0
263
+ while index < len(text):
264
+ char = text[index]
265
+ if char == "\\":
266
+ index += 2
267
+ continue
268
+ if char == "[":
269
+ index = _class_end(text, index) + 1
270
+ continue
271
+ if char == "(":
272
+ if text[index : index + 2] == "(?" and text[index : index + 3] != "(?:":
273
+ return False
274
+ depth += 1
275
+ elif char == ")":
276
+ depth -= 1
277
+ elif char == "|" and depth == 0:
278
+ return False
279
+ elif char == "$" and index == len(text) - 1:
280
+ return depth == 0
281
+ index += 1
282
+ return False
283
+
284
+
285
+ def _class_end(text: str, start: int) -> int:
286
+ index = start + 1
287
+ if text[index] == "^":
288
+ index += 1
289
+ if text[index] == "]":
290
+ index += 1
291
+ while index < len(text):
292
+ if text[index] == "\\":
293
+ index += 2
294
+ continue
295
+ if text[index] == "]":
296
+ return index
297
+ index += 1
298
+ return len(text)