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.
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/PKG-INFO +1 -1
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/pyproject.toml +1 -1
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_nominal_project.py +9 -2
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_resource_provenance.py +15 -2
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_loguru_printf_format.py +2 -2
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_offset_pagination.py +30 -1
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_secret_in_log.py +24 -23
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +190 -66
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_str_enum.py +359 -108
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +35 -5
- {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
- {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
- {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
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/.gitignore +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/LICENSE +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/README.md +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {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
- {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
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {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
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {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
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {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
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {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
- {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
- {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
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {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
- {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
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {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
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {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
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {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
- {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
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {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
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {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
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {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.
|
|
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/
|
{sarj_python_lint-1.40.2 → sarj_python_lint-1.41.2}/src/sarj_python_lint/rules/_nominal_project.py
RENAMED
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
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.2}/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):
|