sarj-python-lint 1.40.2__tar.gz → 1.41.2__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.2 → sarj_python_lint-1.41.2}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_nominal_project.py +9 -2
  4. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_resource_provenance.py +15 -2
  5. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +49 -5
  6. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_loguru_printf_format.py +2 -2
  7. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_offset_pagination.py +30 -1
  8. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +2 -2
  9. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_secret_in_log.py +24 -23
  10. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +23 -6
  11. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +190 -66
  12. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_str_enum.py +359 -108
  13. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +35 -5
  14. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +6 -0
  15. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +20 -1
  16. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +20 -2
  17. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +4 -1
  18. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/.gitignore +0 -0
  19. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/LICENSE +0 -0
  20. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/README.md +0 -0
  21. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/__init__.py +0 -0
  22. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/__main__.py +0 -0
  23. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_analysis_session.py +0 -0
  24. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_file_context.py +0 -0
  25. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_filesystem.py +0 -0
  26. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_python_target.py +0 -0
  27. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  28. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_secret_names.py +0 -0
  29. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_source.py +0 -0
  30. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_version.py +0 -0
  31. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/benchmark.py +0 -0
  32. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/interpreter_argv.py +0 -0
  33. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/json_boundary.py +0 -0
  34. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/py.typed +0 -0
  35. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/ratchet.py +0 -0
  36. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rule_base.py +0 -0
  37. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/__init__.py +0 -0
  38. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  39. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  40. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  41. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  42. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_comments.py +0 -0
  43. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  44. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  45. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  46. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  47. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_exports.py +0 -0
  48. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  49. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_first_party.py +0 -0
  50. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  51. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_imports.py +0 -0
  52. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  53. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_local_source.py +0 -0
  54. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_logging.py +0 -0
  55. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  56. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_paths.py +0 -0
  57. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_project_index.py +0 -0
  58. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  59. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  60. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_pytest.py +0 -0
  61. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_registry.py +0 -0
  62. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  63. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  64. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_sql.py +0 -0
  65. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  66. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  67. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  68. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  69. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  70. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  71. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  72. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  73. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  74. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  75. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  76. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  77. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  78. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  79. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  80. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  81. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  82. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  83. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  84. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  85. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  86. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  87. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  88. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  89. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  90. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  91. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  92. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  93. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  94. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  95. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  96. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  97. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  98. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  99. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  100. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  101. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  102. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  103. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  104. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  105. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  106. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  107. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  108. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  109. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  110. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  111. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  112. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  113. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  114. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  115. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  116. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  117. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  118. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  119. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  120. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  121. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  122. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  123. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  124. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  125. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  126. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  127. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  128. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  129. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  130. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  131. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  132. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  133. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  134. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  135. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  136. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  137. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  138. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  139. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  140. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  141. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  142. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  143. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  144. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  145. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  146. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  147. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  148. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  149. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  150. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  151. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  152. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  153. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  154. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  155. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  156. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  157. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  158. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  159. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  160. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  161. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  162. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  163. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  164. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  165. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  166. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  167. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  168. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  169. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  170. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  171. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  172. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  173. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  174. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  175. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  176. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  177. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  178. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  179. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  180. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  181. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  182. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  183. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  184. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  185. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  186. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  187. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  188. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  189. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  190. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  191. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  192. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  193. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  194. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  195. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  196. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  197. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  198. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/stepdown.py +0 -0
  199. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  200. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  201. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  202. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  203. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  204. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  205. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  206. {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/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.2
3
+ Version: 1.41.2
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.2"
3
+ version = "1.41.2"
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" }]
@@ -12,6 +12,7 @@ from packaging.requirements import InvalidRequirement, Requirement
12
12
  from packaging.utils import canonicalize_name
13
13
 
14
14
  from sarj_python_lint._source import read_python_source
15
+ from sarj_python_lint.rules._ast_index import NodeIndex
15
16
  from sarj_python_lint.rules._first_party import distribution_root
16
17
  from sarj_python_lint.rules._imports import ImportIndex
17
18
  from sarj_python_lint.rules._paths import is_generated
@@ -33,6 +34,11 @@ class NominalSource:
33
34
  path: Path
34
35
  text: str
35
36
 
37
+ @cached_property
38
+ def node_index(self) -> NodeIndex | None:
39
+ tree = self.tree
40
+ return NodeIndex(tree) if tree is not None else None
41
+
36
42
  @cached_property
37
43
  def tree(self) -> ast.Module | None:
38
44
  try:
@@ -49,12 +55,13 @@ class NominalSource:
49
55
  @cached_property
50
56
  def imports(self) -> ImportIndex:
51
57
  tree = self.tree
52
- if tree is None:
58
+ node_index = self.node_index
59
+ if tree is None or node_index is None:
53
60
  return ImportIndex(MappingProxyType({}), frozenset())
54
61
  imports = ImportIndex.from_tree(tree)
55
62
  mutated = {
56
63
  ast.unparse(node).partition(".")[0]
57
- for node in ast.walk(tree)
64
+ for node in node_index.query((ast.Attribute,))
58
65
  if isinstance(node, ast.Attribute) and isinstance(node.ctx, (ast.Store, ast.Del))
59
66
  }
60
67
  return ImportIndex(
@@ -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):