sarj-python-lint 1.40.2__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.
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/PKG-INFO +1 -1
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/pyproject.toml +1 -1
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_resource_provenance.py +15 -2
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +49 -5
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_loguru_printf_format.py +2 -2
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_offset_pagination.py +30 -1
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +2 -2
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_secret_in_log.py +24 -23
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +23 -6
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +35 -5
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +6 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +20 -1
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +20 -2
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +4 -1
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/.gitignore +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/LICENSE +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/README.md +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.40.2 → 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.
|
|
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/
|
|
@@ -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
|
|
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
|
|
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
|
|
41
|
-
"
|
|
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 =
|
|
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
|
-
|
|
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 "
|
|
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
|
-
|
|
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"\
|
|
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():
|
{sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/no_secret_in_log.py
RENAMED
|
@@ -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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
193
|
-
diags.
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
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(
|
|
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):
|
|
@@ -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
|
|
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
|
|
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))
|
|
693
|
-
|
|
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*
|
|
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(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/_analysis_session.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/interpreter_argv.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_ast_position.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.40.2 → sarj_python_lint-1.41.1}/src/sarj_python_lint/rules/_closed_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|