sarj-python-lint 1.63.0__tar.gz → 1.64.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/__main__.py +22 -25
- sarj_python_lint-1.64.0/src/sarj_python_lint/cast_access.py +251 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rule_base.py +3 -3
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_registry.py +2 -0
- sarj_python_lint-1.64.0/src/sarj_python_lint/rules/no_blanket_suppression.py +134 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +6 -1
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_offset_pagination.py +32 -10
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_select_star.py +2 -16
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +42 -12
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +1 -8
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +9 -21
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +2 -3
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +2 -2
- sarj_python_lint-1.64.0/src/sarj_python_lint/suppression_policy.py +5 -0
- sarj_python_lint-1.63.0/src/sarj_python_lint/cast_access.py +0 -86
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/.gitignore +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/LICENSE +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/README.md +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.63.0 → sarj_python_lint-1.64.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.64.0
|
|
4
4
|
Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
|
|
5
5
|
Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
|
|
6
6
|
Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
|
|
@@ -23,6 +23,7 @@ from sarj_python_lint.rule_base import Diagnostic, ProjectRule, Rule, Severity,
|
|
|
23
23
|
from sarj_python_lint.rules import REGISTRY
|
|
24
24
|
from sarj_python_lint.rules._paths import clear_path_caches
|
|
25
25
|
from sarj_python_lint.rules._project_index import ProjectIndexSet
|
|
26
|
+
from sarj_python_lint.suppression_policy import PROTECTED_SUPPRESSION_CODES
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
SKIP_DIR_NAMES = frozenset(
|
|
@@ -46,11 +47,17 @@ SKIP_DIR_NAMES = frozenset(
|
|
|
46
47
|
}
|
|
47
48
|
)
|
|
48
49
|
|
|
49
|
-
#
|
|
50
|
-
# hand-written source worth linting.
|
|
50
|
+
# Bound resource use, but never turn an unanalyzed selected file into success.
|
|
51
51
|
_MAX_FILE_BYTES = 500_000
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
def _checked_size(path: Path) -> Path:
|
|
55
|
+
if path.stat().st_size > _MAX_FILE_BYTES:
|
|
56
|
+
msg = f"selected Python source exceeds {_MAX_FILE_BYTES} bytes: {path}; use an explicit path exclusion for generated source"
|
|
57
|
+
raise ValueError(msg)
|
|
58
|
+
return path
|
|
59
|
+
|
|
60
|
+
|
|
54
61
|
def expand_paths(paths: list[Path]) -> list[Path]:
|
|
55
62
|
out: list[Path] = []
|
|
56
63
|
for p in paths:
|
|
@@ -58,11 +65,7 @@ def expand_paths(paths: list[Path]) -> list[Path]:
|
|
|
58
65
|
msg = f"input does not exist: {p}"
|
|
59
66
|
raise ValueError(msg)
|
|
60
67
|
if p.is_file():
|
|
61
|
-
|
|
62
|
-
if p.stat().st_size <= _MAX_FILE_BYTES:
|
|
63
|
-
out.append(p)
|
|
64
|
-
except OSError:
|
|
65
|
-
pass
|
|
68
|
+
out.append(_checked_size(p))
|
|
66
69
|
continue
|
|
67
70
|
out.extend(_python_files(p))
|
|
68
71
|
return out
|
|
@@ -78,11 +81,8 @@ def _python_files(p: Path) -> list[Path]:
|
|
|
78
81
|
if not fnmatch(name, "*.py"):
|
|
79
82
|
continue
|
|
80
83
|
child = directory / name
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
out.append(child)
|
|
84
|
-
except OSError:
|
|
85
|
-
continue
|
|
84
|
+
if child.is_file():
|
|
85
|
+
out.append(_checked_size(child))
|
|
86
86
|
return sorted(out)
|
|
87
87
|
|
|
88
88
|
|
|
@@ -109,12 +109,7 @@ def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
|
|
|
109
109
|
rule.prepare_session(session)
|
|
110
110
|
clear_path_caches()
|
|
111
111
|
expanded = expand_paths(paths)
|
|
112
|
-
loaded:
|
|
113
|
-
for path in expanded:
|
|
114
|
-
try:
|
|
115
|
-
loaded[path] = read_python_source(path)
|
|
116
|
-
except OSError:
|
|
117
|
-
continue
|
|
112
|
+
loaded = {path: read_python_source(path) for path in expanded}
|
|
118
113
|
project_rules = [rule for rule in rules if isinstance(rule, ProjectRule)]
|
|
119
114
|
if project_rules:
|
|
120
115
|
indexes = ProjectIndexSet.build(expanded, loaded, facts=session.first_party)
|
|
@@ -131,14 +126,13 @@ def check_source(
|
|
|
131
126
|
rules: list[Rule], p: Path, source: str, *, session: AnalysisSession | None = None
|
|
132
127
|
) -> list[Diagnostic]:
|
|
133
128
|
context = PythonFileContext(p, source, session)
|
|
129
|
+
if rules and context.tree is None:
|
|
130
|
+
msg = f"selected Python source could not be parsed: {p}"
|
|
131
|
+
raise ValueError(msg)
|
|
134
132
|
source_lines = context.source_lines
|
|
135
133
|
raw = [diagnostic for rule in rules for diagnostic in rule.check_context(context)]
|
|
136
134
|
return deduplicate_diagnostics(
|
|
137
|
-
[
|
|
138
|
-
diagnostic
|
|
139
|
-
for diagnostic in raw
|
|
140
|
-
if diagnostic.code == "SARJ419" or not is_suppressed(source_lines, diagnostic.line, diagnostic.code)
|
|
141
|
-
],
|
|
135
|
+
[diagnostic for diagnostic in raw if not is_suppressed(source_lines, diagnostic.line, diagnostic.code)],
|
|
142
136
|
source=source,
|
|
143
137
|
context=context,
|
|
144
138
|
)
|
|
@@ -157,6 +151,9 @@ def analyze(
|
|
|
157
151
|
|
|
158
152
|
_DIAGNOSTIC_PRECEDENCE = MappingProxyType(
|
|
159
153
|
{
|
|
154
|
+
"SARJ054": frozenset({"SARJ419"}),
|
|
155
|
+
"SARJ448": frozenset({"SARJ419"}),
|
|
156
|
+
"SARJ523": frozenset({"SARJ419"}),
|
|
160
157
|
"SARJ050": frozenset({"SARJ420"}),
|
|
161
158
|
"SARJ084": frozenset({"SARJ050", "SARJ420"}),
|
|
162
159
|
"SARJ085": frozenset({"SARJ420"}),
|
|
@@ -417,7 +414,7 @@ def _explain(wanted: str) -> int:
|
|
|
417
414
|
def _baseline_counts(diags: list[Diagnostic]) -> dict[str, dict[str, int]]:
|
|
418
415
|
counts: dict[str, dict[str, int]] = {}
|
|
419
416
|
for d in diags:
|
|
420
|
-
if d.severity is Severity.WARNING:
|
|
417
|
+
if d.severity is Severity.WARNING or d.code in PROTECTED_SUPPRESSION_CODES:
|
|
421
418
|
continue
|
|
422
419
|
key = _baseline_path(d.path)
|
|
423
420
|
counts.setdefault(key, {})
|
|
@@ -467,7 +464,7 @@ def _apply_baseline(
|
|
|
467
464
|
seen: Counter[tuple[str, str]] = Counter()
|
|
468
465
|
out: list[Diagnostic] = []
|
|
469
466
|
for d in diags:
|
|
470
|
-
if d.severity is Severity.WARNING:
|
|
467
|
+
if d.severity is Severity.WARNING or d.code in PROTECTED_SUPPRESSION_CODES:
|
|
471
468
|
out.append(d)
|
|
472
469
|
continue
|
|
473
470
|
path_key = _baseline_path(d.path, root=root)
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING, Literal
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
8
|
+
from sarj_python_lint.rules._namespace_access import dynamic_namespace_access
|
|
9
|
+
from sarj_python_lint.rules._resource_provenance import ResourceProvenance
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Iterator
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_TYPING = frozenset({"typing", "typing_extensions"})
|
|
18
|
+
_GETATTR_ARITIES = frozenset({2, 3})
|
|
19
|
+
_MAX_LITERAL_DEPTH = 12
|
|
20
|
+
_CAST_LENGTH = len("cast")
|
|
21
|
+
_COMPREHENSIONS = (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class CastAccess:
|
|
26
|
+
node: ast.Call | ast.Subscript | ast.ImportFrom
|
|
27
|
+
qualified_api: str
|
|
28
|
+
kind: Literal["wildcard", "reflection"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def indirect_typing_cast_accesses(path: Path, source: str) -> tuple[CastAccess, ...]:
|
|
32
|
+
context = PythonFileContext(path, source)
|
|
33
|
+
if not any(marker in context.symbol_source for marker in ("*", "getattr", "__dict__", "vars", "__getattribute__")):
|
|
34
|
+
return ()
|
|
35
|
+
if context.tree is None:
|
|
36
|
+
return ()
|
|
37
|
+
accesses = [
|
|
38
|
+
CastAccess(node, f"{node.module}.cast", "wildcard")
|
|
39
|
+
for node in context.nodes(ast.ImportFrom)
|
|
40
|
+
if node.level == 0 and node.module in _TYPING and any(alias.name == "*" for alias in node.names)
|
|
41
|
+
]
|
|
42
|
+
accesses.extend(_reflection_accesses(context))
|
|
43
|
+
return tuple(sorted(accesses, key=lambda access: (access.node.lineno, access.node.col_offset)))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _reflection_accesses(context: PythonFileContext) -> list[CastAccess]:
|
|
47
|
+
provenance = ResourceProvenance(context)
|
|
48
|
+
candidates = [
|
|
49
|
+
(node, receiver)
|
|
50
|
+
for node in context.nodes(ast.Call, ast.Subscript)
|
|
51
|
+
if (receiver := _cast_receiver(node, provenance)) is not None
|
|
52
|
+
]
|
|
53
|
+
if not candidates:
|
|
54
|
+
return []
|
|
55
|
+
receivers = {id(receiver) for _, receiver in candidates}
|
|
56
|
+
proven = [
|
|
57
|
+
(node, module)
|
|
58
|
+
for node, receiver in candidates
|
|
59
|
+
if (module := _typing_receiver(receiver, provenance, receivers)) is not None
|
|
60
|
+
]
|
|
61
|
+
safe_reflection = {id(carrier) for node, _ in proven if (carrier := _reflection_carrier(node)) is not None}
|
|
62
|
+
namespace_hazard = dynamic_namespace_access(
|
|
63
|
+
(node for node in context.nodes(ast.AST) if id(node) not in safe_reflection), context.imports, context.parents
|
|
64
|
+
)
|
|
65
|
+
if provenance.wildcard or namespace_hazard or not provenance.builtin_namespace_untouched:
|
|
66
|
+
return []
|
|
67
|
+
return [CastAccess(node, f"{module}.cast", "reflection") for node, module in proven]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _cast_receiver(node: ast.Call | ast.Subscript, provenance: ResourceProvenance) -> ast.Name | None:
|
|
71
|
+
if isinstance(node, ast.Call):
|
|
72
|
+
return _call_receiver(node, provenance)
|
|
73
|
+
if not isinstance(node.ctx, ast.Load) or _static_text(node.slice) != "cast":
|
|
74
|
+
return None
|
|
75
|
+
namespace = node.value
|
|
76
|
+
if isinstance(namespace, ast.Attribute) and namespace.attr == "__dict__" and isinstance(namespace.value, ast.Name):
|
|
77
|
+
return namespace.value
|
|
78
|
+
if isinstance(namespace, ast.Call) and len(namespace.args) == 1 and _builtin_call(namespace, "vars", provenance):
|
|
79
|
+
return namespace.args[0] if isinstance(namespace.args[0], ast.Name) else None
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _call_receiver(node: ast.Call, provenance: ResourceProvenance) -> ast.Name | None:
|
|
84
|
+
if (
|
|
85
|
+
len(node.args) in _GETATTR_ARITIES
|
|
86
|
+
and _builtin_call(node, "getattr", provenance)
|
|
87
|
+
and _static_text(node.args[1]) == "cast"
|
|
88
|
+
):
|
|
89
|
+
return node.args[0] if isinstance(node.args[0], ast.Name) else None
|
|
90
|
+
if (
|
|
91
|
+
isinstance(node.func, ast.Attribute)
|
|
92
|
+
and node.func.attr == "__getattribute__"
|
|
93
|
+
and len(node.args) == 1
|
|
94
|
+
and not node.keywords
|
|
95
|
+
and _static_text(node.args[0]) == "cast"
|
|
96
|
+
):
|
|
97
|
+
return node.func.value if isinstance(node.func.value, ast.Name) else None
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _builtin_call(call: ast.Call, name: str, provenance: ResourceProvenance) -> bool:
|
|
102
|
+
if not isinstance(call.func, ast.Name) or call.func.id != name or call.keywords:
|
|
103
|
+
return False
|
|
104
|
+
return not _type_parameter_shadowed(name, call.func, provenance) and all(
|
|
105
|
+
name not in provenance.bindings.get(scope, {}) for scope in _lexical_scopes(call.func, provenance)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _lexical_scopes(node: ast.AST, provenance: ResourceProvenance) -> Iterator[ast.AST]:
|
|
110
|
+
scope = _evaluation_scope(node, provenance)
|
|
111
|
+
immediate = True
|
|
112
|
+
while scope is not None:
|
|
113
|
+
# Ordinary nested code blocks do not capture class locals.
|
|
114
|
+
if immediate or not isinstance(scope, ast.ClassDef):
|
|
115
|
+
yield scope
|
|
116
|
+
immediate = False
|
|
117
|
+
scope = _evaluation_scope(scope, provenance)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _evaluation_scope(node: ast.AST, provenance: ResourceProvenance) -> ast.AST | None:
|
|
121
|
+
scope = provenance.scope(node)
|
|
122
|
+
if isinstance(scope, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef) and _owner_annotation(
|
|
123
|
+
node, scope, provenance
|
|
124
|
+
):
|
|
125
|
+
node, scope = scope, provenance.scope(scope)
|
|
126
|
+
while isinstance(scope, _COMPREHENSIONS):
|
|
127
|
+
iterable = scope.generators[0].iter
|
|
128
|
+
if iterable is not node and not provenance.contains(iterable, node):
|
|
129
|
+
break
|
|
130
|
+
# A comprehension's first iterable is evaluated in the enclosing scope.
|
|
131
|
+
node, scope = scope, provenance.scope(scope)
|
|
132
|
+
return scope
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _owner_annotation(
|
|
136
|
+
node: ast.AST, owner: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef, provenance: ResourceProvenance
|
|
137
|
+
) -> bool:
|
|
138
|
+
if any(parameter is node or provenance.contains(parameter, node) for parameter in owner.type_params):
|
|
139
|
+
return True
|
|
140
|
+
return isinstance(owner, ast.FunctionDef | ast.AsyncFunctionDef) and _function_annotation(node, owner, provenance)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _function_annotation(
|
|
144
|
+
node: ast.AST, function: ast.FunctionDef | ast.AsyncFunctionDef, provenance: ResourceProvenance
|
|
145
|
+
) -> bool:
|
|
146
|
+
current = node
|
|
147
|
+
while (parent := provenance.context.parents.get(current)) is not None:
|
|
148
|
+
if (parent is function and current is function.returns) or (
|
|
149
|
+
isinstance(parent, ast.arg) and current is parent.annotation
|
|
150
|
+
):
|
|
151
|
+
return True
|
|
152
|
+
if parent is function:
|
|
153
|
+
break
|
|
154
|
+
current = parent
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _type_parameter_shadowed(name: str, node: ast.AST, provenance: ResourceProvenance) -> bool:
|
|
159
|
+
current: ast.AST | None = node
|
|
160
|
+
while current is not None:
|
|
161
|
+
if (
|
|
162
|
+
isinstance(current, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.TypeAlias)
|
|
163
|
+
and any(
|
|
164
|
+
isinstance(parameter, ast.TypeVar | ast.TypeVarTuple | ast.ParamSpec) and parameter.name == name
|
|
165
|
+
for parameter in current.type_params
|
|
166
|
+
)
|
|
167
|
+
and not _outside_parameter_scope(node, current, provenance)
|
|
168
|
+
and not _local_binding_overrides(name, node, current, provenance)
|
|
169
|
+
):
|
|
170
|
+
return True
|
|
171
|
+
current = provenance.context.parents.get(current)
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _outside_parameter_scope(
|
|
176
|
+
node: ast.AST,
|
|
177
|
+
owner: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.TypeAlias,
|
|
178
|
+
provenance: ResourceProvenance,
|
|
179
|
+
) -> bool:
|
|
180
|
+
outside = [] if isinstance(owner, ast.TypeAlias) else list(owner.decorator_list)
|
|
181
|
+
if isinstance(owner, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
182
|
+
outside.extend(owner.args.defaults)
|
|
183
|
+
outside.extend(value for value in owner.args.kw_defaults if value is not None)
|
|
184
|
+
# Defaults and decorators evaluate in the enclosing scope.
|
|
185
|
+
return any(value is node or provenance.contains(value, node) for value in outside)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _local_binding_overrides(name: str, node: ast.AST, owner: ast.AST, provenance: ResourceProvenance) -> bool:
|
|
189
|
+
for scope in _lexical_scopes(node, provenance):
|
|
190
|
+
if scope is not owner and not provenance.contains(owner, scope):
|
|
191
|
+
return False
|
|
192
|
+
if name in provenance.bindings.get(scope, {}):
|
|
193
|
+
return True
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _static_text(node: ast.expr, depth: int = 0) -> str | None:
|
|
198
|
+
# Evaluate only bounded, side-effect-free string syntax, never runtime code.
|
|
199
|
+
if depth > _MAX_LITERAL_DEPTH:
|
|
200
|
+
return None
|
|
201
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
202
|
+
return node.value if len(node.value) <= _CAST_LENGTH else None
|
|
203
|
+
if isinstance(node, ast.JoinedStr):
|
|
204
|
+
# Parsing adjacent static f-strings folds them into one Constant.
|
|
205
|
+
return _static_text(node.values[0], depth + 1) if len(node.values) == 1 else None
|
|
206
|
+
if not isinstance(node, ast.BinOp) or not isinstance(node.op, ast.Add):
|
|
207
|
+
return None
|
|
208
|
+
left, right = _static_text(node.left, depth + 1), _static_text(node.right, depth + 1)
|
|
209
|
+
if left is None or right is None:
|
|
210
|
+
return None
|
|
211
|
+
text = left + right
|
|
212
|
+
return text if len(text) <= _CAST_LENGTH else None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _reflection_carrier(node: ast.Call | ast.Subscript) -> ast.AST | None:
|
|
216
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
|
|
217
|
+
return node.func
|
|
218
|
+
if isinstance(node, ast.Subscript):
|
|
219
|
+
return node.value.func if isinstance(node.value, ast.Call) else node.value
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _typing_receiver(node: ast.Name, provenance: ResourceProvenance, receivers: set[int]) -> str | None:
|
|
224
|
+
if node.id in provenance.mutated | provenance.external_bindings:
|
|
225
|
+
return None
|
|
226
|
+
binding = _name_binding(node, provenance)
|
|
227
|
+
if not (
|
|
228
|
+
isinstance(binding, ast.alias)
|
|
229
|
+
and isinstance(provenance.context.parents.get(binding), ast.Import)
|
|
230
|
+
and binding.name in _TYPING
|
|
231
|
+
):
|
|
232
|
+
return None
|
|
233
|
+
for read in provenance.reads.get(node.id, ()):
|
|
234
|
+
if _name_binding(read, provenance) is not binding:
|
|
235
|
+
continue
|
|
236
|
+
parent = provenance.context.parents.get(read)
|
|
237
|
+
if isinstance(parent, ast.Attribute) and parent.value is read:
|
|
238
|
+
continue
|
|
239
|
+
if id(read) in receivers:
|
|
240
|
+
continue
|
|
241
|
+
return None
|
|
242
|
+
return binding.name
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _name_binding(node: ast.Name, provenance: ResourceProvenance) -> ast.AST | None:
|
|
246
|
+
if _type_parameter_shadowed(node.id, node, provenance):
|
|
247
|
+
return None
|
|
248
|
+
for scope in _lexical_scopes(node, provenance):
|
|
249
|
+
if bindings := provenance.bindings.get(scope, {}).get(node.id):
|
|
250
|
+
return provenance.binding(node.id, bindings[0])
|
|
251
|
+
return None
|
|
@@ -20,6 +20,7 @@ from sarj_rule_contracts import (
|
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
from sarj_python_lint._python_target import PythonTargetFacts
|
|
23
|
+
from sarj_python_lint.suppression_policy import PROTECTED_SUPPRESSION_CODES
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
if TYPE_CHECKING:
|
|
@@ -43,7 +44,7 @@ _SARJ_NOQA_RE = re.compile(
|
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
def is_suppressed(source_lines: Sequence[str], line: int, code: str) -> bool:
|
|
46
|
-
if line < 1 or line > len(source_lines):
|
|
47
|
+
if code.upper() in PROTECTED_SUPPRESSION_CODES or line < 1 or line > len(source_lines):
|
|
47
48
|
return False
|
|
48
49
|
text = source_lines[line - 1]
|
|
49
50
|
m = _SARJ_NOQA_RE.search(text)
|
|
@@ -58,8 +59,7 @@ def is_suppressed(source_lines: Sequence[str], line: int, code: str) -> bool:
|
|
|
58
59
|
codes_str = m.group(1)
|
|
59
60
|
trailing = comment[m.end() :]
|
|
60
61
|
if codes_str is None:
|
|
61
|
-
|
|
62
|
-
return not (trailing and (not trailing[0].isspace() or trailing.lstrip().startswith(":")))
|
|
62
|
+
return False
|
|
63
63
|
reason = trailing.lstrip()
|
|
64
64
|
single_dash_reason = reason.startswith("-") and (codes_str[-1:].isspace() or trailing[:1].isspace())
|
|
65
65
|
if reason and not reason.startswith(("--", "—", "–")) and not single_dash_reason:
|
|
@@ -34,6 +34,7 @@ from sarj_python_lint.rules.no_analytical_aggregation_in_postgres_store import (
|
|
|
34
34
|
from sarj_python_lint.rules.no_any_mapping_types import NoAnyMappingTypes
|
|
35
35
|
from sarj_python_lint.rules.no_async_function_cache import NoAsyncFunctionCache
|
|
36
36
|
from sarj_python_lint.rules.no_before_validator_input_mutation import NoBeforeValidatorInputMutation
|
|
37
|
+
from sarj_python_lint.rules.no_blanket_suppression import NoBlanketSuppression
|
|
37
38
|
from sarj_python_lint.rules.no_broad_keyword_capture import NoBroadKeywordCapture
|
|
38
39
|
from sarj_python_lint.rules.no_comment_cruft import NoCommentCruft
|
|
39
40
|
from sarj_python_lint.rules.no_conftest_test_module_import import NoConftestTestModuleImport
|
|
@@ -329,6 +330,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
329
330
|
RequirePydanticForExternalJson.id: RequirePydanticForExternalJson,
|
|
330
331
|
RequirePydanticForMappingShape.id: RequirePydanticForMappingShape,
|
|
331
332
|
NoDualShapeModelFieldReader.id: NoDualShapeModelFieldReader,
|
|
333
|
+
NoBlanketSuppression.id: NoBlanketSuppression,
|
|
332
334
|
PreferModuleLevelConstant.id: PreferModuleLevelConstant,
|
|
333
335
|
PreferImmutableModuleConstant.id: PreferImmutableModuleConstant,
|
|
334
336
|
PreferInjectedDependencyOverMonkeypatch.id: PreferInjectedDependencyOverMonkeypatch,
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import PurePosixPath
|
|
4
|
+
import re
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint.rule_base import (
|
|
8
|
+
AutofixPolicy,
|
|
9
|
+
ColumnEncoding,
|
|
10
|
+
Diagnostic,
|
|
11
|
+
ExampleFile,
|
|
12
|
+
ExampleOutcome,
|
|
13
|
+
Rule,
|
|
14
|
+
RuleCategory,
|
|
15
|
+
RuleDocumentation,
|
|
16
|
+
RuleExample,
|
|
17
|
+
)
|
|
18
|
+
from sarj_python_lint.rules._suppression_comments import scan_comments_or_none
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_SARJ = re.compile(r"(?:^|#\s*)sarj-noqa\b(?P<tail>[^#]*)", re.IGNORECASE)
|
|
26
|
+
_SARJ_CODES = re.compile(r"^:\s*SARJ\d+\b", re.IGNORECASE)
|
|
27
|
+
_PYRIGHT = re.compile(r"(?:^|#\s*)pyright:\s*(?P<tail>[^#]*)")
|
|
28
|
+
_SCOPED_IGNORE = re.compile(r"^ignore\s*\[\s*report[A-Za-z]+(?:\s*,\s*report[A-Za-z]+)*\s*,?\s*\]")
|
|
29
|
+
_DIAGNOSTIC = re.compile(r"^report[A-Z][A-Za-z]+$")
|
|
30
|
+
_WEAKENED = frozenset({"false", "none", "warning", "information", "hint"})
|
|
31
|
+
_WEAKER_MODES = frozenset({"basic", "standard"})
|
|
32
|
+
_DISABLED_CHECKS = frozenset({"analyzeUnannotatedFunctions", "disableBytesTypePromotions", "strictParameterNoneValue"})
|
|
33
|
+
_RUFF_BROAD = re.compile(
|
|
34
|
+
r"^ruff:\s*(?:file-ignore\s*\[(?P<file>[^\]]+)\]|noqa\s*:\s*(?P<codes>[A-Za-z0-9_, ]+)|disable\s*\[[^\]]+\])",
|
|
35
|
+
re.IGNORECASE,
|
|
36
|
+
)
|
|
37
|
+
_FLAKE8_BROAD = re.compile(r"^flake8:\s*noqa\s*:\s*[A-Za-z][A-Za-z0-9]*", re.IGNORECASE)
|
|
38
|
+
_BAD = "def value() -> str:\n return 1 # pyright: ignore\n"
|
|
39
|
+
_GOOD = "value = vendor.value # pyright: ignore[reportPrivateUsage] -- the dependency exposes this API only privately\n"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@final
|
|
43
|
+
class NoBlanketSuppression(Rule):
|
|
44
|
+
id = "no-blanket-suppression"
|
|
45
|
+
code = "SARJ523"
|
|
46
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
47
|
+
summary="Use named, line-local suppressions instead of blanket, file or range disables.",
|
|
48
|
+
rationale=(
|
|
49
|
+
"A blanket suppression hides unrelated errors on its line. A file-level diagnostic downgrade also "
|
|
50
|
+
"authorizes future defects outside the reviewed exception. Suppression checks cannot waive themselves."
|
|
51
|
+
),
|
|
52
|
+
remediation="Fix the diagnostic or use an exact, line-local rule code with a concrete boundary reason.",
|
|
53
|
+
category=RuleCategory.CORRECTNESS,
|
|
54
|
+
autofix=AutofixPolicy.NONE,
|
|
55
|
+
limitations=(
|
|
56
|
+
"Checks actual Python comment tokens for bare or empty sarj-noqa, unscoped pyright: ignore, basic/standard Pyright modes, report* downgrades below error, analyzeUnannotatedFunctions/disableBytesTypePromotions/strictParameterNoneValue=false and scoped Ruff/Flake8 file or Ruff range disables. SARJ054 retains precedence for Ruff banned-api file directives.",
|
|
57
|
+
"Ruff owns blanket noqa and type: ignore through PGH004 and PGH003. Exact local exceptions remain available; this rule does not judge whether their explanations justify the exception.",
|
|
58
|
+
"Directive examples inside strings and unrelated Pyright feature flags are excluded. Source-generated markers cannot waive accountability; use explicit whole-path exclusions for generated/vendor code. Consumer type-checker configuration remains outside this source rule.",
|
|
59
|
+
"The full Standards gate protects this rule and the existing suppression-accountability checks from inline waivers, rule exclusions and diagnostic baselines. Explicitly selected partial checks remain scoped.",
|
|
60
|
+
),
|
|
61
|
+
examples=(
|
|
62
|
+
RuleExample(
|
|
63
|
+
example_id="blanket-type-ignore",
|
|
64
|
+
title="An unscoped ignore hides every diagnostic on the line",
|
|
65
|
+
outcome=ExampleOutcome.MATCH,
|
|
66
|
+
files=(ExampleFile.python("app/adapter.py", _BAD),),
|
|
67
|
+
focus_path=PurePosixPath("app/adapter.py"),
|
|
68
|
+
expected_count=1,
|
|
69
|
+
public=True,
|
|
70
|
+
),
|
|
71
|
+
RuleExample(
|
|
72
|
+
example_id="exact-local-exception",
|
|
73
|
+
title="A named diagnostic retains the dependency boundary explanation",
|
|
74
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
75
|
+
files=(ExampleFile.python("app/adapter.py", _GOOD),),
|
|
76
|
+
focus_path=PurePosixPath("app/adapter.py"),
|
|
77
|
+
expected_count=0,
|
|
78
|
+
public=True,
|
|
79
|
+
),
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
description = documentation.summary
|
|
83
|
+
|
|
84
|
+
@override
|
|
85
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
86
|
+
findings: list[Diagnostic] = []
|
|
87
|
+
for comment in scan_comments_or_none(context.source) or ():
|
|
88
|
+
bare_sarj = any(_SARJ_CODES.match(match["tail"]) is None for match in _SARJ.finditer(comment.body))
|
|
89
|
+
broad_pyright = any(
|
|
90
|
+
_broad_pyright(match["tail"].strip(), configuration=match.start() == 0)
|
|
91
|
+
for match in _PYRIGHT.finditer(comment.body)
|
|
92
|
+
)
|
|
93
|
+
broad_ruff = _broad_ruff(comment.body) if comment.standalone else False
|
|
94
|
+
if bare_sarj or broad_pyright or broad_ruff:
|
|
95
|
+
findings.append(
|
|
96
|
+
Diagnostic(
|
|
97
|
+
context.path,
|
|
98
|
+
comment.line,
|
|
99
|
+
comment.col,
|
|
100
|
+
self.code,
|
|
101
|
+
"Use a named, line-local suppression with a concrete reason; blanket ignores and "
|
|
102
|
+
"file/range disables hide unrelated diagnostics.",
|
|
103
|
+
column_encoding=ColumnEncoding.CODEPOINTS,
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
return findings
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _broad_ruff(body: str) -> bool:
|
|
110
|
+
if _FLAKE8_BROAD.match(body) is not None:
|
|
111
|
+
return True
|
|
112
|
+
match = _RUFF_BROAD.match(body)
|
|
113
|
+
if match is None:
|
|
114
|
+
return False
|
|
115
|
+
selectors = (match["file"] or match["codes"] or "").upper()
|
|
116
|
+
return not {"TID251", "BANNED-API"}.intersection(part.strip() for part in selectors.split(","))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _broad_pyright(tail: str, *, configuration: bool) -> bool:
|
|
120
|
+
if re.match(r"ignore(?=$|\s|\[)", tail) is not None:
|
|
121
|
+
return _SCOPED_IGNORE.match(tail) is None
|
|
122
|
+
if not configuration:
|
|
123
|
+
return False
|
|
124
|
+
options = tail.split("--", 1)[0].split("—", 1)[0].split(",")
|
|
125
|
+
for raw in options:
|
|
126
|
+
option = raw.strip()
|
|
127
|
+
if option in _WEAKER_MODES:
|
|
128
|
+
return True
|
|
129
|
+
name, separator, value = option.partition("=")
|
|
130
|
+
if separator and name.strip() in _DISABLED_CHECKS and value.strip() == "false":
|
|
131
|
+
return True
|
|
132
|
+
if separator and _DIAGNOSTIC.fullmatch(name.strip()) and value.strip() in _WEAKENED:
|
|
133
|
+
return True
|
|
134
|
+
return False
|
|
@@ -235,7 +235,12 @@ def _folded_seconds(node: ast.expr | None, constants: Mapping[str, float]) -> fl
|
|
|
235
235
|
return None if value is None else -value
|
|
236
236
|
case ast.BinOp(left=left, op=op, right=right):
|
|
237
237
|
lhs, rhs = _folded_seconds(left, constants), _folded_seconds(right, constants)
|
|
238
|
-
|
|
238
|
+
if lhs is None or rhs is None:
|
|
239
|
+
return None
|
|
240
|
+
try:
|
|
241
|
+
return _arithmetic(op, lhs, rhs)
|
|
242
|
+
except OverflowError:
|
|
243
|
+
return None
|
|
239
244
|
case _:
|
|
240
245
|
return None
|
|
241
246
|
|
|
@@ -60,7 +60,7 @@ class NoOffsetPagination(Rule):
|
|
|
60
60
|
category=RuleCategory.PERFORMANCE,
|
|
61
61
|
autofix=AutofixPolicy.NONE,
|
|
62
62
|
limitations=(
|
|
63
|
-
"Only SQL string literals in recognized store modules are analyzed.",
|
|
63
|
+
"Only SQL string literals, f-strings, and their static concatenations in recognized store modules are analyzed.",
|
|
64
64
|
"Only dynamic OFFSET values in credible SELECT queries are reported; fixed ordinal offsets are excluded.",
|
|
65
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.",
|
|
66
66
|
"Generated files, migrations, docstrings, comments, quoted identifiers, and BigQuery `WITH OFFSET AS` array indexing are excluded.",
|
|
@@ -95,6 +95,36 @@ class NoOffsetPagination(Rule):
|
|
|
95
95
|
expected_count=0,
|
|
96
96
|
public=True,
|
|
97
97
|
),
|
|
98
|
+
RuleExample(
|
|
99
|
+
example_id="concatenated-dynamic-offset",
|
|
100
|
+
title="Concatenation retains a dynamic offset",
|
|
101
|
+
outcome=ExampleOutcome.MATCH,
|
|
102
|
+
files=(
|
|
103
|
+
ExampleFile.python(
|
|
104
|
+
"app/task_store.py",
|
|
105
|
+
"QUERY = 'SELECT id FROM task ORDER BY id ' + f'LIMIT 10 OFFSET {offset}'\n",
|
|
106
|
+
),
|
|
107
|
+
),
|
|
108
|
+
focus_path=PurePosixPath("app/task_store.py"),
|
|
109
|
+
expected_count=1,
|
|
110
|
+
scenario="concatenated-sql",
|
|
111
|
+
public=True,
|
|
112
|
+
),
|
|
113
|
+
RuleExample(
|
|
114
|
+
example_id="concatenated-static-offset",
|
|
115
|
+
title="A dynamic limit does not make the offset dynamic",
|
|
116
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
117
|
+
files=(
|
|
118
|
+
ExampleFile.python(
|
|
119
|
+
"app/task_store.py",
|
|
120
|
+
"QUERY = f'SELECT id FROM task LIMIT {limit} ' + 'OFFSET 2'\n",
|
|
121
|
+
),
|
|
122
|
+
),
|
|
123
|
+
focus_path=PurePosixPath("app/task_store.py"),
|
|
124
|
+
expected_count=0,
|
|
125
|
+
scenario="concatenated-sql",
|
|
126
|
+
public=True,
|
|
127
|
+
),
|
|
98
128
|
),
|
|
99
129
|
)
|
|
100
130
|
description = documentation.summary
|
|
@@ -168,15 +198,7 @@ def _has_dynamic_numeric_offset(sql: str) -> bool:
|
|
|
168
198
|
|
|
169
199
|
|
|
170
200
|
def _sql_value(node: ast.expr) -> str | None:
|
|
171
|
-
|
|
172
|
-
pieces: list[str] = []
|
|
173
|
-
for value in node.values:
|
|
174
|
-
if isinstance(value, ast.Constant) and isinstance(value.value, str):
|
|
175
|
-
pieces.append(value.value)
|
|
176
|
-
elif isinstance(value, ast.FormattedValue):
|
|
177
|
-
pieces.append(":__sarj_dynamic__")
|
|
178
|
-
return "".join(pieces)
|
|
179
|
-
return sql_string_value(node)
|
|
201
|
+
return sql_string_value(node, interpolation_placeholder=":__sarj_dynamic__")
|
|
180
202
|
|
|
181
203
|
|
|
182
204
|
def _mask_dialect_noise(sql: str) -> str:
|