sarj-python-lint 1.40.1__tar.gz → 1.41.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 (206) hide show
  1. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_resource_provenance.py +15 -2
  4. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +49 -5
  5. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_loguru_printf_format.py +2 -2
  6. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_offset_pagination.py +30 -1
  7. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +2 -2
  8. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_secret_in_log.py +24 -23
  9. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +23 -6
  10. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +20 -10
  11. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +35 -5
  12. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +6 -0
  13. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +20 -1
  14. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +20 -2
  15. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +4 -1
  16. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/.gitignore +0 -0
  17. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/LICENSE +0 -0
  18. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/README.md +0 -0
  19. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/__init__.py +0 -0
  20. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/__main__.py +0 -0
  21. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/_analysis_session.py +0 -0
  22. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/_file_context.py +0 -0
  23. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/_filesystem.py +0 -0
  24. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/_python_target.py +0 -0
  25. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  26. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/_secret_names.py +0 -0
  27. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/_source.py +0 -0
  28. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/_version.py +0 -0
  29. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/benchmark.py +0 -0
  30. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/interpreter_argv.py +0 -0
  31. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/json_boundary.py +0 -0
  32. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/py.typed +0 -0
  33. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/ratchet.py +0 -0
  34. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rule_base.py +0 -0
  35. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/__init__.py +0 -0
  36. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  37. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  38. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  39. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  40. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_comments.py +0 -0
  41. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  42. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  43. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  44. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  45. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_exports.py +0 -0
  46. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  47. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
  48. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  49. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_imports.py +0 -0
  50. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  51. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_local_source.py +0 -0
  52. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_logging.py +0 -0
  53. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  54. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  55. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_paths.py +0 -0
  56. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_project_index.py +0 -0
  57. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  58. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  59. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
  60. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_registry.py +0 -0
  61. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  62. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  63. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_sql.py +0 -0
  64. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  65. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  66. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  67. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  68. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  69. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  70. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  71. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  72. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  73. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  74. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  75. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  76. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  77. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  78. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  79. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  80. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  81. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  82. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  83. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  84. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  85. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  86. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  87. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  88. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  89. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  90. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  91. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  92. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  93. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  94. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  95. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  96. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  97. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  98. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  99. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  100. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  101. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  102. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  103. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  104. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  105. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  106. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  107. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  108. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  109. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  110. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  111. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  112. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  113. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  114. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  115. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  116. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  117. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  118. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  119. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  120. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  121. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  122. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  123. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  124. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  125. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  126. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  127. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  128. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  129. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  130. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  131. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  132. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  133. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  134. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  135. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  136. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  137. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  138. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  139. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  140. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  141. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  142. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  143. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  144. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  145. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  146. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  147. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  148. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  149. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  150. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  151. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  152. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  153. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  154. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  155. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  156. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  157. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  158. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  159. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  160. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  161. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  162. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  163. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  164. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  165. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  166. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  167. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  168. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  169. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  170. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  171. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  172. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  173. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  174. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  175. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  176. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  177. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  178. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  179. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  180. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  181. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  182. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  183. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  184. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  185. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  186. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  187. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  188. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  189. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  190. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  191. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  192. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  193. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  194. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  195. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  196. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  197. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  198. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
  199. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  200. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  201. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  202. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  203. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  204. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  205. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  206. {sarj_python_lint-1.40.1 → sarj_python_lint-1.41.1}/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.40.1
3
+ Version: 1.41.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/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.40.1"
3
+ version = "1.41.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" }]
@@ -95,9 +95,22 @@ class ResourceProvenance:
95
95
 
96
96
  def scope(self, node: ast.AST) -> ast.AST | None:
97
97
  parent = self.context.parents.get(node)
98
- while parent is not None and not isinstance(parent, _SCOPES):
98
+ while parent is not None:
99
+ if isinstance(parent, ast.arguments) and (node in parent.defaults or node in parent.kw_defaults):
100
+ parent = self.context.parents.get(parent)
101
+ elif isinstance(parent, _SCOPES) and not (
102
+ (
103
+ isinstance(parent, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef)
104
+ and node in parent.decorator_list
105
+ )
106
+ or (isinstance(parent, ast.ClassDef) and (node in parent.bases or node in parent.keywords))
107
+ ):
108
+ return parent
109
+ if parent is None:
110
+ return None
111
+ node = parent
99
112
  parent = self.context.parents.get(parent)
100
- return parent
113
+ return None
101
114
 
102
115
  def _binding_scope(self, node: ast.AST) -> ast.AST | None:
103
116
  scope = self.scope(node)
@@ -14,7 +14,6 @@ from sarj_python_lint.rule_base import (
14
14
  RuleDocumentation,
15
15
  RuleExample,
16
16
  )
17
- from sarj_python_lint.rules._ast_index import walk
18
17
 
19
18
 
20
19
  if TYPE_CHECKING:
@@ -37,8 +36,8 @@ class NoCorsWildcardWithCredentials(Rule):
37
36
  category=RuleCategory.SECURITY,
38
37
  autofix=AutofixPolicy.NONE,
39
38
  limitations=(
40
- 'The rule requires literal `True` for `allow_credentials` and either a literal `"*"` below `allow_origins` or an exact universal `allow_origin_regex` literal.',
41
- "Dynamically computed credential flags and origin collections are not resolved.",
39
+ 'The rule requires literal `True` for `allow_credentials` and either a direct `"*"` origin in a literal collection or an exact universal `allow_origin_regex` literal. Literal keyword dictionaries and their effective nested unpacks are inspected.',
40
+ "Conditional origin collections are checked in each possible branch. Calls, comprehensions, nested non-string elements, dynamic keyword unpacks, computed credential flags, and variable origin collections are not resolved.",
42
41
  ),
43
42
  examples=(
44
43
  RuleExample(
@@ -86,7 +85,7 @@ class NoCorsWildcardWithCredentials(Rule):
86
85
  for node in context.nodes(ast.Call):
87
86
  if not _is_cors_construction(node, imports):
88
87
  continue
89
- keywords = {arg: kw.value for kw in node.keywords if (arg := kw.arg) is not None}
88
+ keywords = _effective_keywords(node)
90
89
  credentials = keywords.get("allow_credentials")
91
90
  origins = keywords.get("allow_origins")
92
91
  origin_regex = keywords.get("allow_origin_regex")
@@ -121,7 +120,52 @@ def _is_true_literal(node: ast.expr) -> bool:
121
120
 
122
121
 
123
122
  def _contains_star_literal(node: ast.expr) -> bool:
124
- return any(isinstance(child, ast.Constant) and child.value == "*" for child in walk(node))
123
+ match node:
124
+ case ast.Constant(value="*"):
125
+ return True
126
+ case ast.List(elts=elements) | ast.Tuple(elts=elements) | ast.Set(elts=elements):
127
+ return any(
128
+ (isinstance(element, ast.Constant) and element.value == "*")
129
+ or (isinstance(element, ast.Starred) and _contains_star_literal(element.value))
130
+ for element in elements
131
+ )
132
+ case ast.IfExp(body=body, orelse=orelse):
133
+ return _contains_star_literal(body) or _contains_star_literal(orelse)
134
+ case ast.BoolOp(values=values):
135
+ return any(_contains_star_literal(value) for value in values)
136
+ case _:
137
+ return False
138
+
139
+
140
+ def _effective_keywords(node: ast.Call) -> dict[str, ast.expr]:
141
+ values: dict[str, ast.expr] = {}
142
+ for keyword in node.keywords:
143
+ if keyword.arg is not None:
144
+ values[keyword.arg] = keyword.value
145
+ elif isinstance(keyword.value, ast.Dict):
146
+ literal_values = _literal_dictionary_values(keyword.value)
147
+ if literal_values is None:
148
+ values.clear()
149
+ else:
150
+ values.update(literal_values)
151
+ else:
152
+ values.clear()
153
+ return values
154
+
155
+
156
+ def _literal_dictionary_values(node: ast.Dict) -> dict[str, ast.expr] | None:
157
+ values: dict[str, ast.expr] = {}
158
+ for key, value in zip(node.keys, node.values, strict=True):
159
+ if isinstance(key, ast.Constant) and isinstance(key.value, str):
160
+ values[key.value] = value
161
+ elif key is None and isinstance(value, ast.Dict):
162
+ nested_values = _literal_dictionary_values(value)
163
+ if nested_values is None:
164
+ return None
165
+ values.update(nested_values)
166
+ else:
167
+ return None
168
+ return values
125
169
 
126
170
 
127
171
  def _is_cors_construction(node: ast.Call, imports: ImportIndex) -> bool:
@@ -49,7 +49,7 @@ class NoLoguruPrintfFormat(Rule):
49
49
  autofix=AutofixPolicy.NONE,
50
50
  limitations=(
51
51
  "Only unambiguous module-level Loguru imports and immediate bind/opt chains with a literal message and supplied positional formatting arguments are checked.",
52
- "The message must contain a valid percent conversion and no brace fields. Escaped percent signs alone, malformed templates, dynamic messages and expanded arguments are excluded.",
52
+ "The decoded literal message must contain a valid percent conversion and no brace fields; Unicode and hex escapes preserve conversions. Escaped percent signs alone, malformed templates, dynamic messages and expanded arguments are excluded.",
53
53
  "Standard logging, unknown receivers, stored aliases, shadowed or escaped imports, custom wrapper chains, keyword-only fields and generated files are excluded.",
54
54
  "Visible namespace or logger mutations invalidate import proof. Interprocedural effects and runtime monkeypatches are not resolved.",
55
55
  "Argument types and printf arity are not inferred. The warning requests review; no automatic conversion changes formatting or structured logging contracts.",
@@ -89,7 +89,7 @@ class NoLoguruPrintfFormat(Rule):
89
89
 
90
90
  @override
91
91
  def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
92
- if "%" not in context.source or "loguru" not in context.source or context.generated or context.tree is None:
92
+ if "loguru" not in context.source or context.generated or context.tree is None:
93
93
  return []
94
94
  provenance: ResourceProvenance | None = None
95
95
  diagnostics: list[Diagnostic] = []
@@ -30,6 +30,13 @@ _OFFSET_PAGINATION = re.compile(
30
30
  re.IGNORECASE,
31
31
  )
32
32
  _MYSQL_OFFSET_PAGINATION = re.compile(rf"\bLIMIT\s+{_DYNAMIC_VALUE}\s*,", re.IGNORECASE)
33
+ _OFFSET_START = re.compile(r"\bOFFSET\b\s+", re.IGNORECASE)
34
+ _NUMERIC_OFFSET_TOKEN = re.compile(
35
+ rf"{_DYNAMIC_VALUE}|\d+(?:\.\d+)?|(?:GREATEST|LEAST|COALESCE|ABS|CEIL|CEILING|FLOOR)\b|[-()+*/%,]",
36
+ re.IGNORECASE,
37
+ )
38
+ _DYNAMIC_TOKEN = re.compile(_DYNAMIC_VALUE)
39
+ _OFFSET_END = re.compile(r"\s*(?:ROWS?\b|FETCH\b|FOR\b|LIMIT\b|[);]|$)", re.IGNORECASE)
33
40
  _QUERY_SHAPE = re.compile(r"\bSELECT\b[\s\S]*?\bFROM\b", re.IGNORECASE)
34
41
  _MIGRATION_PARTS = frozenset({"migration", "migrations"})
35
42
  _QUOTED_IDENTIFIER = re.compile(r"`[^`\n]*`|\[[^\]\n]*\]")
@@ -55,6 +62,7 @@ class NoOffsetPagination(Rule):
55
62
  limitations=(
56
63
  "Only SQL string literals in recognized store modules are analyzed.",
57
64
  "Only dynamic OFFSET values in credible SELECT queries are reported; fixed ordinal offsets are excluded.",
65
+ "Parenthesized arithmetic and common numeric wrappers such as GREATEST, LEAST, and COALESCE preserve the dynamic driver marker. Arbitrary SQL functions and procedural values remain opaque.",
58
66
  "Generated files, migrations, docstrings, comments, quoted identifiers, and BigQuery `WITH OFFSET AS` array indexing are excluded.",
59
67
  "ORM pagination APIs are outside this embedded-SQL rule.",
60
68
  ),
@@ -117,7 +125,7 @@ class NoOffsetPagination(Rule):
117
125
 
118
126
  sql = _mask_dialect_noise(strip_sql_noise(text))
119
127
  if _QUERY_SHAPE.search(sql) is None or (
120
- _OFFSET_PAGINATION.search(sql) is None and _MYSQL_OFFSET_PAGINATION.search(sql) is None
128
+ not _has_dynamic_numeric_offset(sql) and _MYSQL_OFFSET_PAGINATION.search(sql) is None
121
129
  ):
122
130
  continue
123
131
 
@@ -138,6 +146,27 @@ class NoOffsetPagination(Rule):
138
146
  return diags
139
147
 
140
148
 
149
+ def _has_dynamic_numeric_offset(sql: str) -> bool:
150
+ if _OFFSET_PAGINATION.search(sql) is not None:
151
+ return True
152
+ for start in _OFFSET_START.finditer(sql):
153
+ cursor, depth, dynamic = start.end(), 0, False
154
+ while cursor < len(sql):
155
+ if sql[cursor].isspace():
156
+ cursor += 1
157
+ continue
158
+ token = _NUMERIC_OFFSET_TOKEN.match(sql, cursor)
159
+ if token is None or (token.group() == ")" and depth == 0):
160
+ break
161
+ value = token.group()
162
+ depth += int(value == "(") - int(value == ")")
163
+ dynamic |= _DYNAMIC_TOKEN.fullmatch(value) is not None
164
+ cursor = token.end()
165
+ if dynamic and depth == 0 and _OFFSET_END.match(sql, cursor) is not None:
166
+ return True
167
+ return False
168
+
169
+
141
170
  def _sql_value(node: ast.expr) -> str | None:
142
171
  if isinstance(node, ast.JoinedStr):
143
172
  pieces: list[str] = []
@@ -22,7 +22,7 @@ if TYPE_CHECKING:
22
22
  from sarj_python_lint._file_context import PythonFileContext
23
23
 
24
24
 
25
- _RANDOM_UUID_RE = re.compile(r"\b(?:gen_random_uuid|uuid_generate_v4)\s*\(", re.IGNORECASE)
25
+ _RANDOM_UUID_RE = re.compile(r"(?<![\w$])(?:gen_random_uuid|uuid_generate_v4)\s*\(", re.IGNORECASE)
26
26
 
27
27
 
28
28
  @dataclass(frozen=True, slots=True)
@@ -155,7 +155,7 @@ def _inside_uuidv7_builder(sql: str, position: int) -> bool:
155
155
 
156
156
  def _identifier_end(sql: str, index: int, position: int) -> _IdentifierEnd:
157
157
  end = index + 1
158
- while end < position and (sql[end].isalnum() or sql[end] == "_"):
158
+ while end < position and (sql[end].isalnum() or sql[end] in {"_", "$"}):
159
159
  end += 1
160
160
  after = end
161
161
  while after < position and sql[after].isspace():
@@ -18,6 +18,7 @@ from sarj_python_lint.rules._logging import LOG_METHODS, is_logger_expr
18
18
 
19
19
 
20
20
  if TYPE_CHECKING:
21
+ from collections.abc import Iterable
21
22
  from pathlib import Path
22
23
 
23
24
  from sarj_python_lint._file_context import PythonFileContext
@@ -53,19 +54,31 @@ def _is_secret_keyword(name: str) -> bool:
53
54
 
54
55
 
55
56
  def _unsafe_message_values(value: ast.expr) -> tuple[ast.expr, ...]:
57
+ if _is_raw_secret_reference(value):
58
+ return (value,)
56
59
  match value:
57
60
  case ast.JoinedStr(values=parts):
58
61
  return _unsafe_formatted_values(parts)
62
+ case ast.BinOp(op=ast.Add(), left=left, right=right):
63
+ return (*_unsafe_message_values(left), *_unsafe_message_values(right))
64
+ case ast.List(elts=elements) | ast.Tuple(elts=elements) | ast.Set(elts=elements):
65
+ return tuple(secret for element in elements for secret in _unsafe_message_values(element))
66
+ case ast.Dict():
67
+ return _literal_mapping_secret_values(value)
59
68
  case ast.BinOp(op=ast.Mod(), right=right):
60
69
  values = right.elts if isinstance(right, (ast.Tuple, ast.List)) else (right,)
61
- return tuple(item for item in values if _is_raw_secret_reference(item))
70
+ return _raw_secret_values(values)
62
71
  case ast.Call(func=ast.Attribute(value=ast.Constant(value=str()), attr="format"), args=args, keywords=keywords):
63
72
  values = (*args, *(keyword.value for keyword in keywords))
64
- return tuple(item for item in values if _is_raw_secret_reference(item))
73
+ return _raw_secret_values(values)
65
74
  case _:
66
75
  return ()
67
76
 
68
77
 
78
+ def _raw_secret_values(values: Iterable[ast.expr]) -> tuple[ast.expr, ...]:
79
+ return tuple(value for value in values if _is_raw_secret_reference(value))
80
+
81
+
69
82
  class NoSecretInLog(Rule):
70
83
  id: str = "no-secret-in-log"
71
84
  code: str = "SARJ012"
@@ -75,7 +88,7 @@ class NoSecretInLog(Rule):
75
88
  remediation="Omit the credential or log only approved non-sensitive metadata through a centralized sanitizer.",
76
89
  category=RuleCategory.SECURITY,
77
90
  limitations=(
78
- "Detection covers direct secret-named names, terminal attributes, constant secret-key subscripts, identity slices, f-string and literal format arguments, nested literal extra mappings, literal keyword unpacking, and immediate structured-logger bindings.",
91
+ "Detection covers direct secret-named message and argument values, terminal attributes, constant secret-key subscripts, identity slices, f-string and literal format arguments, string concatenation, nested literal containers, literal keyword unpacking, and immediate structured-logger bindings.",
79
92
  "Logger recognition and secret classification are lexical; general aliases, arbitrary calls, dynamic containers, and interprocedural dataflow are not inspected.",
80
93
  ),
81
94
  examples=(
@@ -118,7 +131,7 @@ class NoSecretInLog(Rule):
118
131
  if not isinstance(function, ast.Attribute) or not _is_logging_call(node):
119
132
  continue
120
133
  message_index = 1 if function.attr == "log" else 0
121
- for arg in node.args:
134
+ for arg in node.args[message_index:]:
122
135
  diags.extend(
123
136
  Diagnostic(
124
137
  path=path,
@@ -129,11 +142,6 @@ class NoSecretInLog(Rule):
129
142
  )
130
143
  for unsafe in _unsafe_message_values(arg)
131
144
  )
132
- diags.extend(
133
- _diagnostic(path, arg, self.code, "positional logging argument")
134
- for arg in node.args[message_index + 1 :]
135
- if _is_raw_secret_reference(arg)
136
- )
137
145
  _collect_keyword_secrets(path, node, self.code, diags)
138
146
  diags.extend(
139
147
  _diagnostic(path, value, self.code, "structured logger binding")
@@ -147,7 +155,7 @@ def _bound_logger_values(receiver: ast.expr) -> tuple[ast.expr, ...]:
147
155
  return ()
148
156
  if receiver.func.attr not in {"bind", "contextualize"}:
149
157
  return ()
150
- return tuple(keyword.value for keyword in receiver.keywords if _is_raw_secret_reference(keyword.value))
158
+ return tuple(secret for keyword in receiver.keywords for secret in _unsafe_message_values(keyword.value))
151
159
 
152
160
 
153
161
  def _diagnostic(path: Path, value: ast.expr, code: str, context: str) -> Diagnostic:
@@ -189,22 +197,15 @@ def _collect_keyword_secrets(path: Path, node: ast.Call, code: str, diags: list[
189
197
  _diagnostic(path, value, code, "literal `extra` field")
190
198
  for value in _literal_mapping_secret_values(kw.value)
191
199
  )
192
- elif _is_raw_secret_reference(kw.value):
193
- diags.append(_diagnostic(path, kw.value, code, f"`{kw.arg}` logging field"))
200
+ else:
201
+ diags.extend(
202
+ _diagnostic(path, value, code, f"`{kw.arg}` logging field")
203
+ for value in _unsafe_message_values(kw.value)
204
+ )
194
205
 
195
206
 
196
207
  def _literal_mapping_secret_values(mapping: ast.Dict) -> tuple[ast.expr, ...]:
197
- return tuple(
198
- secret
199
- for _, value in _literal_mapping_items(mapping)
200
- for secret in (
201
- _literal_mapping_secret_values(value)
202
- if isinstance(value, ast.Dict)
203
- else (value,)
204
- if _is_raw_secret_reference(value)
205
- else ()
206
- )
207
- )
208
+ return tuple(secret for _, value in _literal_mapping_items(mapping) for secret in _unsafe_message_values(value))
208
209
 
209
210
 
210
211
  def _literal_mapping_items(mapping: ast.Dict) -> tuple[tuple[str, ast.expr], ...]:
@@ -63,6 +63,7 @@ _ENV_CALLS = frozenset({"getenv"})
63
63
  _ENV_MAPPINGS = frozenset({"environ"})
64
64
  _EQUALITY_DUNDERS = frozenset({"__eq__", "__ne__"})
65
65
  _PAIR_OPERAND_COUNT = 2
66
+ _ENCODE_ARGUMENT_LIMIT = 2
66
67
  _SCOPE_NODES = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)
67
68
 
68
69
 
@@ -108,7 +109,7 @@ class PreferConstantTimeSecretCompare(Rule):
108
109
  category=RuleCategory.SECURITY,
109
110
  limitations=(
110
111
  "The diagnostic requires evidence of an externally supplied credential and configured or stored secret state.",
111
- "Only direct equality and unambiguous same-scope aliases from headers, cookies, settings, or environment are traced.",
112
+ "Adjacent equality pairs, literal encode normalization, and unambiguous same-scope aliases from headers, cookies, settings, or environment are traced.",
112
113
  "Generated files, tests, test-support modules, equality methods, public sentinels, and type-like values are excluded.",
113
114
  "`compare_digest` requires compatible ASCII string or bytes-like operands and may still reveal type or length.",
114
115
  ),
@@ -162,9 +163,7 @@ class PreferConstantTimeSecretCompare(Rule):
162
163
  dunder_compares = _equality_dunder_compares(tree, source, node_index=context.node_index)
163
164
  diagnostics: list[Diagnostic] = []
164
165
  for comparison in context.nodes(ast.Compare):
165
- if id(comparison) in dunder_compares or len(comparison.ops) != 1:
166
- continue
167
- if not isinstance(comparison.ops[0], (ast.Eq, ast.NotEq)):
166
+ if id(comparison) in dunder_compares:
168
167
  continue
169
168
  operands = [comparison.left, *comparison.comparators]
170
169
  if _is_password_confirmation_pair(operands):
@@ -176,7 +175,8 @@ class PreferConstantTimeSecretCompare(Rule):
176
175
  position = (comparison.lineno, comparison.col_offset)
177
176
  dominant_roles = _dominating_walrus_roles(comparison, parents, roles, position)
178
177
  operand_roles = [_operand_role(operand, roles, position, dominant_roles) for operand in operands]
179
- if not _has_authentication_pair(operand_roles):
178
+ unsafe_pair = _unsafe_authentication_pair(comparison, operands, operand_roles)
179
+ if unsafe_pair is None:
180
180
  continue
181
181
  diagnostics.append(
182
182
  Diagnostic(
@@ -185,12 +185,24 @@ class PreferConstantTimeSecretCompare(Rule):
185
185
  col=comparison.col_offset + 1,
186
186
  code=self.code,
187
187
  severity=Severity.ERROR,
188
- message=_message(operand_roles),
188
+ message=_message(unsafe_pair),
189
189
  )
190
190
  )
191
191
  return sorted(diagnostics, key=lambda item: (item.line, item.col))
192
192
 
193
193
 
194
+ def _unsafe_authentication_pair(
195
+ comparison: ast.Compare, operands: list[ast.expr], operand_roles: list[_Role]
196
+ ) -> list[_Role] | None:
197
+ for index, operation in enumerate(comparison.ops):
198
+ if not isinstance(operation, (ast.Eq, ast.NotEq)):
199
+ continue
200
+ pair = operand_roles[index : index + 2]
201
+ if _has_authentication_pair(pair) and not _is_password_confirmation_pair(operands[index : index + 2]):
202
+ return pair
203
+ return None
204
+
205
+
194
206
  def _message(roles: list[_Role]) -> str:
195
207
  if any(role.password for role in roles):
196
208
  return "password equality may leak information; use the password-hashing library's verification API"
@@ -274,6 +286,11 @@ def _operand_role(
274
286
  for value in node.values
275
287
  if isinstance(value, ast.FormattedValue)
276
288
  )
289
+ case ast.Call(func=ast.Attribute(value=value, attr="encode"), args=arguments, keywords=[]):
290
+ if len(arguments) <= _ENCODE_ARGUMENT_LIMIT and all(
291
+ isinstance(argument, ast.Constant) and isinstance(argument.value, str) for argument in arguments
292
+ ):
293
+ return _operand_role(value, roles, position, dominant_roles)
277
294
  case _:
278
295
  pass
279
296
  if _is_external_lookup(node):
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import ast
4
+ from collections import Counter
4
5
  from dataclasses import dataclass
5
6
  from pathlib import PurePosixPath
6
7
  import re
@@ -710,7 +711,8 @@ def _unambiguous_exports(tree: ast.Module, imports: ImportIndex, facts: _TypeFac
710
711
  assignments.setdefault(assignment.target, []).append(assignment)
711
712
  # Imports are re-exports only when their binding is unambiguous; local
712
713
  # aliases must have exactly one top-level assignment and no other writes.
713
- unique = {name for name, values in assignments.items() if len(values) == 1 and _binding_count(tree, name) == 1}
714
+ bindings: Counter[str] = _binding_counts(tree) if assignments else Counter()
715
+ unique = {name for name, values in assignments.items() if len(values) == 1 and bindings.get(name, 0) == 1}
714
716
  allowed = unique | set(imports.bindings)
715
717
  return _TypeFacts(
716
718
  {name: shape for name, shape in facts.raw_aliases.items() if name in allowed},
@@ -718,15 +720,23 @@ def _unambiguous_exports(tree: ast.Module, imports: ImportIndex, facts: _TypeFac
718
720
  )
719
721
 
720
722
 
721
- def _binding_count(tree: ast.Module, name: str) -> int:
722
- return sum(
723
- 1
724
- for node in walk_ast(tree)
725
- if (isinstance(node, ast.Name) and isinstance(node.ctx, (ast.Store, ast.Del)) and node.id == name)
726
- or (isinstance(node, ast.arg) and node.arg == name)
727
- or (isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) and node.name == name)
728
- or (isinstance(node, ast.alias) and (node.asname or node.name.partition(".")[0]) == name)
729
- )
723
+ def _binding_counts(tree: ast.Module) -> Counter[str]:
724
+ bindings: Counter[str] = Counter()
725
+ for node in walk_ast(tree):
726
+ match node:
727
+ case (
728
+ ast.Name(id=name, ctx=ast.Store() | ast.Del())
729
+ | ast.arg(arg=name)
730
+ | ast.FunctionDef(name=name)
731
+ | ast.AsyncFunctionDef(name=name)
732
+ | ast.ClassDef(name=name)
733
+ ):
734
+ bindings[name] += 1
735
+ case ast.alias(name=name, asname=alias):
736
+ bindings[alias or name.partition(".")[0]] += 1
737
+ case _:
738
+ continue
739
+ return bindings
730
740
 
731
741
 
732
742
  def _imported_facts(
@@ -128,6 +128,9 @@ class PreferWalrusRegexMatch(Rule):
128
128
  tree = context.tree
129
129
  if tree is None:
130
130
  return []
131
+ candidate_bodies = _candidate_bodies(tree)
132
+ if not candidate_bodies:
133
+ return []
131
134
 
132
135
  source_lines = context.source_lines
133
136
  diags: list[Diagnostic] = []
@@ -153,11 +156,7 @@ class PreferWalrusRegexMatch(Rule):
153
156
  self.code,
154
157
  )
155
158
 
156
- for node in walk(tree):
157
- raw_body: object = getattr(node, "body", None)
158
- if not object_list(raw_body):
159
- continue
160
- body: list[ast.stmt] = [st for st in raw_body if isinstance(st, ast.stmt)]
159
+ for node, body in candidate_bodies:
161
160
  diags.extend(_check_body(node, body, analysis_context))
162
161
 
163
162
  if diags and context.session.python_target.has_declared_support_before(path, (3, 8)):
@@ -165,6 +164,37 @@ class PreferWalrusRegexMatch(Rule):
165
164
  return sorted(diags, key=lambda d: (d.line, d.col))
166
165
 
167
166
 
167
+ def _candidate_bodies(tree: ast.Module) -> list[tuple[ast.AST, list[ast.stmt]]]:
168
+ candidates: list[tuple[ast.AST, list[ast.stmt]]] = []
169
+ for node in walk(tree):
170
+ raw_body: object = getattr(node, "body", None)
171
+ if not object_list(raw_body):
172
+ continue
173
+ body = [statement for statement in raw_body if isinstance(statement, ast.stmt)]
174
+ if any(_is_candidate_pair(body[index], body[index + 1]) for index in range(len(body) - 1)):
175
+ candidates.append((node, body))
176
+ return candidates
177
+
178
+
179
+ def _is_candidate_pair(assignment: ast.stmt, condition: ast.stmt) -> bool:
180
+ match assignment, condition:
181
+ case (
182
+ ast.Assign(
183
+ targets=[ast.Name(id=name)],
184
+ value=ast.Call(func=ast.Attribute(value=ast.Name(), attr=method)),
185
+ ),
186
+ ast.If(test=test),
187
+ ):
188
+ return (
189
+ method in _MATCH_METHODS
190
+ and assignment.end_lineno == assignment.lineno
191
+ and condition.lineno == assignment.lineno + 1
192
+ and _condition_kind(test, name) is not None
193
+ )
194
+ case _:
195
+ return False
196
+
197
+
168
198
  def _check_body(node: ast.AST, body: list[ast.stmt], context: _AnalysisContext) -> list[Diagnostic]:
169
199
  owner = _lexical_owner(node, context.parents)
170
200
  state = _body_state(node, owner, body, context)
@@ -213,6 +213,12 @@ class RequirePydanticForExternalJson(Rule):
213
213
  if tree is None:
214
214
  return []
215
215
  imports = context.imports
216
+ if not any(
217
+ _is_json_loads(call, imports)
218
+ or (isinstance(call.func, ast.Attribute) and call.func.attr == "json" and not call.args)
219
+ for call in context.nodes(ast.Call)
220
+ ):
221
+ return []
216
222
  source_lines = context.source_lines
217
223
  summaries = _module_summaries(tree, imports, source_lines, node_index=context.node_index)
218
224
  findings: list[tuple[ast.expr, ast.Call]] = []
@@ -188,11 +188,21 @@ class RequirePydanticOrdinalLowerBound(Rule):
188
188
  tree = context.tree
189
189
  if tree is None:
190
190
  return []
191
+ candidates = tuple(
192
+ node
193
+ for node in tree.body
194
+ if isinstance(node, ast.ClassDef) and any(_is_ordinal_field(field) for field in node.body)
195
+ )
196
+ if not candidates:
197
+ return []
191
198
  imports = context.module_imports
199
+ models = tuple(cls for cls in candidates if _is_model(cls, imports))
200
+ if not models:
201
+ return []
192
202
  aliases = _annotation_aliases(tree)
193
203
  source_lines = context.source_lines
194
204
  diagnostics: list[Diagnostic] = []
195
- for cls in (node for node in tree.body if isinstance(node, ast.ClassDef) and _is_model(node, imports)):
205
+ for cls in models:
196
206
  diagnostics.extend(
197
207
  _ordinal_class_findings(
198
208
  cls,
@@ -211,6 +221,15 @@ def _is_model(cls: ast.ClassDef, imports: ImportIndex) -> bool:
211
221
  return any(imports.resolves(base, sources=_PYDANTIC_MODEL_SOURCES, symbol="BaseModel") for base in cls.bases)
212
222
 
213
223
 
224
+ def _is_ordinal_field(field: ast.stmt) -> bool:
225
+ return (
226
+ isinstance(field, ast.AnnAssign)
227
+ and isinstance(field.target, ast.Name)
228
+ and not field.target.id.startswith("_")
229
+ and _is_ordinal_field_name(field.target.id)
230
+ )
231
+
232
+
214
233
  def _annotation_aliases(tree: ast.Module) -> dict[str, ast.expr]:
215
234
  binding_counts = _module_binding_counts(tree)
216
235
  candidates: dict[str, list[ast.expr]] = {}
@@ -113,6 +113,7 @@ class StoreGetDelegatesToBulkRead(Rule):
113
113
  "Wrappers with try/finally or other unsupported compound control flow are not diagnosed.",
114
114
  "Singleton implementations with explicit cache or lock identifiers are excluded because their access path differs intentionally.",
115
115
  "Signature compatibility is advisory: dynamic helpers and behavior not visible in the two method bodies require review.",
116
+ "Closed literal starred collection expansions preserve singleton delegation; unknown iterable expansion or additional keys do not prove the singleton contract.",
116
117
  ),
117
118
  aliases=("get-delegates-to-get-many",),
118
119
  examples=(
@@ -689,8 +690,10 @@ def _symbol(expression: ast.expr, bindings: dict[str, str]) -> str:
689
690
  return bindings.get(expression.id, f"unknown:{expression.id}")
690
691
  if isinstance(expression, ast.Attribute):
691
692
  return f"attr:{_qualified_name(expression)}"
692
- if isinstance(expression, (ast.List, ast.Tuple, ast.Set)) and len(expression.elts) == 1:
693
- return f"one:{_symbol(expression.elts[0], bindings)}"
693
+ if isinstance(expression, (ast.List, ast.Tuple, ast.Set)):
694
+ elements = _closed_collection_elements(expression)
695
+ if elements is not None and len(elements) == 1:
696
+ return f"one:{_symbol(elements[0], bindings)}"
694
697
  if (
695
698
  isinstance(expression, ast.Call)
696
699
  and isinstance(expression.func, ast.Name)
@@ -702,6 +705,21 @@ def _symbol(expression: ast.expr, bindings: dict[str, str]) -> str:
702
705
  return f"unknown:{ast.dump(expression, include_attributes=False)}"
703
706
 
704
707
 
708
+ def _closed_collection_elements(expression: ast.List | ast.Tuple | ast.Set) -> list[ast.expr] | None:
709
+ elements: list[ast.expr] = []
710
+ for item in expression.elts:
711
+ if not isinstance(item, ast.Starred):
712
+ elements.append(item)
713
+ elif isinstance(item.value, (ast.List, ast.Tuple, ast.Set)):
714
+ nested = _closed_collection_elements(item.value)
715
+ if nested is None:
716
+ return None
717
+ elements.extend(nested)
718
+ else:
719
+ return None
720
+ return elements
721
+
722
+
705
723
  def _compatible_pair(
706
724
  owner: ast.ClassDef,
707
725
  ) -> _MethodPair | None:
@@ -29,7 +29,9 @@ if TYPE_CHECKING:
29
29
 
30
30
  # Strict keyword adjacency distinguishes SQL writes from prose.
31
31
  _INSERT_WRITE = re.compile(
32
- r"\bINSERT\s+(?:OR\s+\w+\s+)?INTO\s+[\w.\"'`?$:@-]+\s*(?:\([^)]*\)\s*)?(?:VALUES|SELECT|DEFAULT\s+VALUES)\b",
32
+ r"\bINSERT\s+(?:OR\s+\w+\s+)?INTO\s+[\w.\"'`?$:@-]+\s*"
33
+ r"(?:AS\s+(?:[A-Za-z_][\w$]*|\"(?:\"\"|[^\"])+\")\s*)?(?:\([^)]*\)\s*)?"
34
+ r"(?:OVERRIDING\s+(?:SYSTEM|USER)\s+VALUE\s+)?(?:VALUES|SELECT|DEFAULT\s+VALUES)\b",
33
35
  re.IGNORECASE,
34
36
  )
35
37
 
@@ -86,6 +88,7 @@ class StoreInsertRequiresOnConflict(Rule):
86
88
  "explicitly replay-named callables in recognized store modules are analyzed."
87
89
  ),
88
90
  "Dynamic SQL policy composition and ordinary create, seed, migration, schedule, and upsert methods are excluded.",
91
+ "Target AS aliases and OVERRIDING clauses preserve INSERT detection. Conflict detection remains lexical within one SQL statement; per-CTE ownership is not established by this embedded-SQL rule.",
89
92
  ),
90
93
  examples=(
91
94
  RuleExample(