sarj-python-lint 1.25.7__tar.gz → 1.26.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/pyproject.toml +1 -1
  3. sarj_python_lint-1.26.1/src/sarj_python_lint/interpreter_argv.py +377 -0
  4. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_registry.py +2 -0
  5. sarj_python_lint-1.26.1/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +191 -0
  6. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/.gitignore +0 -0
  7. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/LICENSE +0 -0
  8. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/README.md +0 -0
  9. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/__init__.py +0 -0
  10. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/__main__.py +0 -0
  11. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/_analysis_session.py +0 -0
  12. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/_file_context.py +0 -0
  13. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/_filesystem.py +0 -0
  14. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/_python_target.py +0 -0
  15. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  16. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/_secret_names.py +0 -0
  17. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/_source.py +0 -0
  18. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/_version.py +0 -0
  19. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/benchmark.py +0 -0
  20. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/json_boundary.py +0 -0
  21. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/py.typed +0 -0
  22. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/ratchet.py +0 -0
  23. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rule_base.py +0 -0
  24. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/__init__.py +0 -0
  25. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  26. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  27. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  28. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_comments.py +0 -0
  29. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  30. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  31. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  32. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  33. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  34. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
  35. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  36. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_imports.py +0 -0
  37. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_local_source.py +0 -0
  38. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_logging.py +0 -0
  39. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  40. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_paths.py +0 -0
  41. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_project_index.py +0 -0
  42. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  43. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
  44. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  45. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  46. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_sql.py +0 -0
  47. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  48. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  49. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  50. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  51. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  52. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  53. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  54. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  55. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  56. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  57. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  58. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  59. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  60. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  61. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  62. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  63. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  64. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  65. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  66. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  67. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  68. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  69. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  70. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  71. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  72. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  73. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  74. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  75. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  76. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  77. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  78. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  79. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  80. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  81. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  82. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  83. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  84. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  85. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  86. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  87. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  88. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  89. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  90. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  91. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  92. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  93. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  94. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  95. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  96. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  97. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  98. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  99. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  100. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  101. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  102. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  103. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  104. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  105. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  106. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  107. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  108. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  109. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  110. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  111. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  112. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  113. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  114. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  115. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  116. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  117. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  118. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  119. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  120. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  121. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  122. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  123. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  124. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  125. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  126. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  127. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  128. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  129. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  130. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  131. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  132. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  133. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  134. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  135. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  136. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  137. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  138. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  139. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  140. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  141. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  142. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  143. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  144. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  145. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  146. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  147. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  148. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  149. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  150. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  151. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  152. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  153. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  154. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  155. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  156. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  157. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  158. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  159. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  160. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  161. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  162. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  163. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  164. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  165. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  166. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  167. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  168. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  169. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  170. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  171. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  172. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  173. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  174. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  175. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  176. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
  177. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  178. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  179. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  180. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  181. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  182. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  183. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  184. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  185. {sarj_python_lint-1.25.7 → sarj_python_lint-1.26.1}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.25.7
3
+ Version: 1.26.1
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
6
6
  Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.25.7"
3
+ version = "1.26.1"
4
4
  description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "sarj-ai" }]
@@ -0,0 +1,377 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import PurePosixPath
5
+ import re
6
+ from types import MappingProxyType
7
+ from typing import TYPE_CHECKING, Literal
8
+
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Sequence
12
+
13
+
14
+ _SHORT_ATTACHED_LENGTH = 2
15
+
16
+
17
+ ProgramKind = Literal["external", "inline", "shell", "stdin", "other", "unknown"]
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class InterpreterInvocation:
22
+ kind: ProgramKind
23
+ payload: str | None = None
24
+ forwarded: tuple[str, ...] = ()
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class ParsedOption:
29
+ invocation: InterpreterInvocation | None = None
30
+ consumed: int = 1
31
+
32
+
33
+ _SHELLS = frozenset({"sh", "bash", "dash", "zsh", "ksh"})
34
+ _NODE_VALUE_FLAGS = frozenset(
35
+ {
36
+ "--require",
37
+ "--import",
38
+ "--loader",
39
+ "--experimental-loader",
40
+ "--input-type",
41
+ "--conditions",
42
+ "--inspect-port",
43
+ "--openssl-config",
44
+ "--icu-data-dir",
45
+ "--title",
46
+ }
47
+ )
48
+ _JQ_VALUE_FLAGS = MappingProxyType(
49
+ {"--arg": 2, "--argjson": 2, "--slurpfile": 2, "--rawfile": 2, "--indent": 1, "-L": 1}
50
+ )
51
+ _RUBY_VALUE_FLAGS = frozenset({"I", "r", "C", "X", "E"})
52
+ _RUBY_LONG_VALUE_FLAGS = frozenset(
53
+ {"--encoding", "--external-encoding", "--internal-encoding", "--backtrace-limit", "--enable", "--disable"}
54
+ )
55
+ _WRAPPER_VALUES = MappingProxyType(
56
+ {
57
+ "env": frozenset({"-u", "--unset", "-C", "--chdir"}),
58
+ "sudo": frozenset(
59
+ {
60
+ "-u",
61
+ "--user",
62
+ "-g",
63
+ "--group",
64
+ "-h",
65
+ "--host",
66
+ "-p",
67
+ "--prompt",
68
+ "-C",
69
+ "--close-from",
70
+ "-T",
71
+ "--command-timeout",
72
+ "-R",
73
+ "--chroot",
74
+ "-D",
75
+ "--chdir",
76
+ }
77
+ ),
78
+ "timeout": frozenset({"-s", "--signal", "-k", "--kill-after"}),
79
+ }
80
+ )
81
+ _MAX_WRAPPERS = 8
82
+
83
+
84
+ class UnprovableCommandError(ValueError):
85
+ """A wrapper executable position cannot be established safely."""
86
+
87
+
88
+ def unwrap_command(argv: Sequence[str]) -> tuple[str, ...]:
89
+ current = tuple(argv)
90
+ for _ in range(_MAX_WRAPPERS):
91
+ if not current:
92
+ return ()
93
+ command = PurePosixPath(current[0]).name
94
+ if command in {"exec", "command"}:
95
+ if command == "command" and any(argument in {"-v", "-V"} for argument in current[1:2]):
96
+ return current
97
+ current = current[_command_wrapper_index(current, command) :]
98
+ continue
99
+ if command not in _WRAPPER_VALUES:
100
+ return current
101
+ current = current[_value_wrapper_index(current, command) :]
102
+ msg = "command wrapper nesting exceeds analysis bound"
103
+ raise UnprovableCommandError(msg)
104
+
105
+
106
+ def _command_wrapper_index(arguments: Sequence[str], command: str) -> int:
107
+ index = 1
108
+ while index < len(arguments) and arguments[index].startswith("-"):
109
+ argument = arguments[index]
110
+ if argument == "--":
111
+ return index + 1
112
+ if command == "exec" and argument == "-a":
113
+ index += 2
114
+ continue
115
+ if argument not in {"-p", "-c", "-l"}:
116
+ msg = "unsupported command wrapper option"
117
+ raise UnprovableCommandError(msg)
118
+ index += 1
119
+ return index
120
+
121
+
122
+ def _value_wrapper_index(arguments: Sequence[str], command: str) -> int:
123
+ index = 1
124
+ while index < len(arguments):
125
+ argument = arguments[index]
126
+ if argument == "--":
127
+ index += 1
128
+ break
129
+ if command == "env" and re.match(r"^[A-Za-z_][A-Za-z0-9_]*=", argument):
130
+ index += 1
131
+ continue
132
+ if not argument.startswith("-") or argument == "-":
133
+ break
134
+ if command == "env" and argument in {"-S", "--split-string"}:
135
+ msg = "env split-string has a separate unproven quoting grammar"
136
+ raise UnprovableCommandError(msg)
137
+ index += 2 if argument in _WRAPPER_VALUES[command] else 1
138
+ # Timeout's positional duration precedes its executable.
139
+ return index + int(command == "timeout")
140
+
141
+
142
+ def classify_interpreter(argv: Sequence[str]) -> InterpreterInvocation:
143
+ try:
144
+ arguments = unwrap_command(argv)
145
+ except UnprovableCommandError:
146
+ return InterpreterInvocation("unknown")
147
+ if not arguments:
148
+ return InterpreterInvocation("other")
149
+ executable = PurePosixPath(arguments[0]).name
150
+ if re.fullmatch(r"python(?:[23](?:\.\d+)?)?", executable):
151
+ return _python(arguments[1:])
152
+ if executable in _SHELLS:
153
+ return _short_options(arguments[1:], source="c", values=frozenset({"o", "O"}), shell_mode=True)
154
+ if executable in {"node", "nodejs"}:
155
+ return _node(arguments[1:])
156
+ if executable == "jq":
157
+ return _jq(arguments[1:])
158
+ if executable in {"awk", "gawk", "mawk", "nawk"}:
159
+ return _awk(arguments[1:])
160
+ if executable == "ruby":
161
+ return _ruby(arguments[1:])
162
+ if executable in {"perl", "php"}:
163
+ return _short_options(arguments[1:], source={"perl": "eE", "php": "r"}[executable], values=frozenset({"I"}))
164
+ return InterpreterInvocation("other")
165
+
166
+
167
+ def _python(arguments: Sequence[str]) -> InterpreterInvocation:
168
+ index = 0
169
+ while index < len(arguments):
170
+ boundary = _external_boundary(arguments, index)
171
+ if boundary is not None:
172
+ return boundary
173
+ argument = arguments[index]
174
+ if argument.startswith("--"):
175
+ return InterpreterInvocation(
176
+ "external"
177
+ if argument in {"--help", "--version", "--help-env", "--help-xoptions", "--help-all"}
178
+ else "unknown"
179
+ )
180
+ parsed = _python_short_option(argument, _following_operand(arguments, index))
181
+ if parsed.invocation is not None:
182
+ return parsed.invocation
183
+ index += parsed.consumed
184
+ return InterpreterInvocation("stdin")
185
+
186
+
187
+ def _python_short_option(argument: str, following: str | None) -> ParsedOption:
188
+ for cursor, flag in enumerate(argument[1:], 1):
189
+ if flag in {"h", "V"}:
190
+ return ParsedOption(InterpreterInvocation("external"))
191
+ if flag in {"c", "m", "W", "X"}:
192
+ attached = argument[cursor + 1 :]
193
+ operand = attached or following
194
+ if operand is None:
195
+ return ParsedOption(InterpreterInvocation("unknown"))
196
+ if flag == "c":
197
+ return ParsedOption(InterpreterInvocation("inline", operand))
198
+ if flag == "m":
199
+ return ParsedOption(InterpreterInvocation("external", operand))
200
+ return ParsedOption(consumed=1 + int(not attached))
201
+ if flag not in "bBdEhiIOPqRsSuvVx":
202
+ return ParsedOption(InterpreterInvocation("unknown"))
203
+ return ParsedOption()
204
+
205
+
206
+ def _ruby(arguments: Sequence[str]) -> InterpreterInvocation:
207
+ index = 0
208
+ while index < len(arguments):
209
+ boundary = _external_boundary(arguments, index)
210
+ if boundary is not None:
211
+ return boundary
212
+ parsed = _ruby_option(arguments, index)
213
+ if parsed.invocation is not None:
214
+ return parsed.invocation
215
+ index += parsed.consumed
216
+ return InterpreterInvocation("stdin")
217
+
218
+
219
+ def _ruby_option(arguments: Sequence[str], index: int) -> ParsedOption:
220
+ argument = arguments[index]
221
+ if argument.startswith("--"):
222
+ return _ruby_long_option(argument, _following_operand(arguments, index))
223
+ if re.fullmatch(r"-(?:0[0-7]{0,3}|W[0-2]?)", argument):
224
+ return ParsedOption()
225
+ for cursor, flag in enumerate(argument[1:], 1):
226
+ if flag == "h":
227
+ return ParsedOption(InterpreterInvocation("external"))
228
+ if flag in "Fix":
229
+ # These operands are attached only and consume the rest of this word.
230
+ return ParsedOption()
231
+ if flag == "e" or flag in _RUBY_VALUE_FLAGS:
232
+ if cursor + 1 == len(argument) and _following_operand(arguments, index) is None:
233
+ return ParsedOption(InterpreterInvocation("unknown"))
234
+ return _short_option(arguments, index, source="e", values=_RUBY_VALUE_FLAGS, shell_mode=False)
235
+ if flag not in "acdlnpsSvwU":
236
+ return ParsedOption(InterpreterInvocation("unknown"))
237
+ return ParsedOption()
238
+
239
+
240
+ def _ruby_long_option(argument: str, following: str | None) -> ParsedOption:
241
+ if argument in {"--help", "--version"}:
242
+ return ParsedOption(InterpreterInvocation("external"))
243
+ if argument in {"--debug", "--verbose", "--copyright"}:
244
+ return ParsedOption()
245
+ name, separator, _operand = argument.partition("=")
246
+ if name in _RUBY_LONG_VALUE_FLAGS and (separator or following is not None):
247
+ return ParsedOption(consumed=1 + int(not separator))
248
+ return ParsedOption(InterpreterInvocation("unknown"))
249
+
250
+
251
+ def _short_options(
252
+ arguments: Sequence[str], *, source: str, values: frozenset[str], shell_mode: bool = False
253
+ ) -> InterpreterInvocation:
254
+ index = 0
255
+ while index < len(arguments):
256
+ boundary = _external_boundary(arguments, index, plus_options=True)
257
+ if boundary is not None:
258
+ return boundary
259
+ argument = arguments[index]
260
+ if argument.startswith("--"):
261
+ index += 2 if shell_mode and argument in {"--rcfile", "--init-file"} else 1
262
+ continue
263
+ parsed = _short_option(arguments, index, source=source, values=values, shell_mode=shell_mode)
264
+ if parsed.invocation is not None:
265
+ return parsed.invocation
266
+ index += parsed.consumed
267
+ return InterpreterInvocation("stdin")
268
+
269
+
270
+ def _short_option(
271
+ arguments: Sequence[str], index: int, *, source: str, values: frozenset[str], shell_mode: bool
272
+ ) -> ParsedOption:
273
+ argument = arguments[index]
274
+ for cursor, flag in enumerate(argument[1:], 1):
275
+ if flag in source:
276
+ # Shell -c always consumes the next word, including inside -ceu.
277
+ attached = "" if shell_mode else argument[cursor + 1 :]
278
+ operand = attached or _following_operand(arguments, index)
279
+ forwarded = tuple(arguments[index + 3 :]) if shell_mode else ()
280
+ return ParsedOption(InterpreterInvocation("shell" if shell_mode else "inline", operand, forwarded))
281
+ if flag in values:
282
+ return ParsedOption(consumed=1 + int(cursor + 1 == len(argument)))
283
+ return ParsedOption()
284
+
285
+
286
+ def _node(arguments: Sequence[str]) -> InterpreterInvocation:
287
+ index = 0
288
+ while index < len(arguments):
289
+ boundary = _external_boundary(arguments, index)
290
+ if boundary is not None:
291
+ return boundary
292
+ argument = arguments[index]
293
+ inline = _node_source_option(argument, _following_operand(arguments, index))
294
+ if inline is not None:
295
+ return inline
296
+ index += 2 if argument in _NODE_VALUE_FLAGS or argument in {"-r", "-C"} else 1
297
+ return InterpreterInvocation("stdin")
298
+
299
+
300
+ def _node_source_option(argument: str, following: str | None) -> InterpreterInvocation | None:
301
+ if argument.split("=", 1)[0] in {"--eval", "--print"}:
302
+ operand = argument.split("=", 1)[1] if "=" in argument else following
303
+ return InterpreterInvocation("inline", operand)
304
+ if argument.startswith("--"):
305
+ return None
306
+ for flag in argument[1:]:
307
+ if flag in "ep":
308
+ return InterpreterInvocation("inline")
309
+ if flag in "rC":
310
+ break
311
+ return None
312
+
313
+
314
+ def _external_boundary(
315
+ arguments: Sequence[str], index: int, *, plus_options: bool = False
316
+ ) -> InterpreterInvocation | None:
317
+ argument = arguments[index]
318
+ if argument == "--":
319
+ return InterpreterInvocation("external" if index + 1 < len(arguments) else "stdin")
320
+ if argument == "-":
321
+ return InterpreterInvocation("stdin")
322
+ prefixes = ("-", "+") if plus_options else ("-",)
323
+ return None if argument.startswith(prefixes) else InterpreterInvocation("external")
324
+
325
+
326
+ def _following_operand(arguments: Sequence[str], index: int) -> str | None:
327
+ return arguments[index + 1] if index + 1 < len(arguments) else None
328
+
329
+
330
+ def _jq(arguments: Sequence[str]) -> InterpreterInvocation:
331
+ index = 0
332
+ while index < len(arguments):
333
+ argument = arguments[index]
334
+ if (
335
+ argument in {"-f", "--from-file"}
336
+ or argument.startswith("--from-file=")
337
+ or (argument.startswith("-f") and len(argument) > _SHORT_ATTACHED_LENGTH)
338
+ ):
339
+ return InterpreterInvocation("external")
340
+ if argument.startswith("-") and not argument.startswith("--") and "f" in argument[1:]:
341
+ return InterpreterInvocation("external")
342
+ if argument == "--":
343
+ return InterpreterInvocation("inline" if index + 1 < len(arguments) else "other")
344
+ if not argument.startswith("-"):
345
+ return InterpreterInvocation("inline", argument)
346
+ index += 1 + _JQ_VALUE_FLAGS.get(argument, 0)
347
+ return InterpreterInvocation("other")
348
+
349
+
350
+ def _awk(arguments: Sequence[str]) -> InterpreterInvocation:
351
+ index = 0
352
+ external = False
353
+ while index < len(arguments):
354
+ argument = arguments[index]
355
+ if argument in {"-f", "--file", "-E", "--exec"}:
356
+ external = True
357
+ index += 2
358
+ continue
359
+ if argument.startswith(("--file=", "--exec=")) or (
360
+ argument.startswith(("-f", "-E")) and len(argument) > _SHORT_ATTACHED_LENGTH
361
+ ):
362
+ external = True
363
+ elif (
364
+ argument in {"-e", "--source"}
365
+ or argument.startswith("--source=")
366
+ or (argument.startswith("-e") and len(argument) > _SHORT_ATTACHED_LENGTH)
367
+ ):
368
+ return InterpreterInvocation("inline")
369
+ elif argument in {"-F", "-v", "--field-separator", "--assign"}:
370
+ index += 2
371
+ continue
372
+ elif argument == "--":
373
+ return InterpreterInvocation("external" if external else "inline")
374
+ elif not argument.startswith("-"):
375
+ return InterpreterInvocation("external" if external else "inline", argument)
376
+ index += 1
377
+ return InterpreterInvocation("external" if external else "other")
@@ -62,6 +62,7 @@ from sarj_python_lint.rules.no_hidden_constructor_fallback import (
62
62
  )
63
63
  from sarj_python_lint.rules.no_injected_module_loader import NoInjectedModuleLoader
64
64
  from sarj_python_lint.rules.no_input_model_mutation import NoInputModelMutation
65
+ from sarj_python_lint.rules.no_interpreter_source_arguments import NoInterpreterSourceArguments
65
66
  from sarj_python_lint.rules.no_invalid_argument_name_suppression import NoInvalidArgumentNameSuppression
66
67
  from sarj_python_lint.rules.no_mocked_pydantic_value_object import NoMockedPydanticValueObject
67
68
  from sarj_python_lint.rules.no_nested_pydantic_field_validator import NoNestedPydanticFieldValidator
@@ -207,6 +208,7 @@ if TYPE_CHECKING:
207
208
 
208
209
  REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
209
210
  {
211
+ NoInterpreterSourceArguments.id: NoInterpreterSourceArguments,
210
212
  NoUnusedCopyResult.id: NoUnusedCopyResult,
211
213
  NoBeforeValidatorInputMutation.id: NoBeforeValidatorInputMutation,
212
214
  NoInputModelMutation.id: NoInputModelMutation,
@@ -0,0 +1,191 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass
5
+ from pathlib import PurePosixPath
6
+ from typing import TYPE_CHECKING, ClassVar, final, override
7
+
8
+ from sarj_python_lint.interpreter_argv import classify_interpreter
9
+ from sarj_python_lint.rule_base import (
10
+ AutofixPolicy,
11
+ Diagnostic,
12
+ ExampleFile,
13
+ ExampleOutcome,
14
+ ProjectRule,
15
+ RuleCategory,
16
+ RuleDocumentation,
17
+ RuleExample,
18
+ Severity,
19
+ is_suppressed,
20
+ )
21
+
22
+
23
+ if TYPE_CHECKING:
24
+ from sarj_python_lint._file_context import PythonFileContext
25
+
26
+
27
+ _SUBPROCESS = frozenset(
28
+ {"subprocess.run", "subprocess.call", "subprocess.check_call", "subprocess.check_output", "subprocess.Popen"}
29
+ )
30
+ _EXEC_VECTOR = frozenset({"os.execv", "os.execve", "os.execvp", "os.execvpe", "os.posix_spawn", "os.posix_spawnp"})
31
+ _EXEC_LIST = frozenset({"os.execl", "os.execle", "os.execlp", "os.execlpe"})
32
+ _SPAWN_VECTOR = frozenset({"os.spawnv", "os.spawnve", "os.spawnvp", "os.spawnvpe"})
33
+ _SPAWN_LIST = frozenset({"os.spawnl", "os.spawnle", "os.spawnlp", "os.spawnlpe"})
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class ProcessArguments:
38
+ argv: ast.expr | None
39
+ executable: ast.expr | None = None
40
+
41
+
42
+ @final
43
+ class NoInterpreterSourceArguments(ProjectRule):
44
+ id = "no-interpreter-source-arguments"
45
+ code = "SARJ484"
46
+ documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
47
+ default_level=Severity.ERROR,
48
+ summary="Keep interpreter programs in linted files or modules, outside process argv.",
49
+ rationale="Moving deployment logic from YAML into Python still bypasses language checks when the Python wrapper passes program source through interpreter flags or jq/awk argv.",
50
+ remediation="Invoke a repository-owned file or module covered by the normal language checks; pass configuration as arguments rather than interpreter source.",
51
+ category=RuleCategory.ARCHITECTURE,
52
+ autofix=AutofixPolicy.NONE,
53
+ limitations=(
54
+ "Only import-proven subprocess process APIs, asyncio.create_subprocess_exec, and os exec/spawn argv APIs with literal string vectors are checked. Import aliases are followed; shadowed or reassigned imports and methods abstain.",
55
+ "Script/module and interpreter option boundaries use the shared dependency-free classifier, including env/sudo/timeout wrappers and jq/awk file flags.",
56
+ "Dynamic argv, shell strings and shell=True, recursive shell payloads, receiver-based remote execution, and unknown interpreter option grammars are outside this narrow rule. A clean result does not prove those programs are linted.",
57
+ "Generated/vendor sources are excluded; maintained tests are checked. Source-checker fixtures may use an exact reasoned suppression.",
58
+ ),
59
+ examples=(
60
+ RuleExample(
61
+ example_id="interpreter-source-argument",
62
+ title="Process argv contains unchecked Python source",
63
+ outcome=ExampleOutcome.MATCH,
64
+ files=(
65
+ ExampleFile.python(
66
+ "tools/check.py",
67
+ "import subprocess\nsubprocess.run(['python3', '-c', 'print(1)'], check=True)\n",
68
+ ),
69
+ ),
70
+ focus_path=PurePosixPath("tools/check.py"),
71
+ expected_count=1,
72
+ public=True,
73
+ ),
74
+ RuleExample(
75
+ example_id="external-interpreter-module",
76
+ title="Process argv invokes a checked module",
77
+ outcome=ExampleOutcome.NO_MATCH,
78
+ files=(
79
+ ExampleFile.python(
80
+ "tools/check.py",
81
+ "import subprocess\nsubprocess.run(['python3', '-m', 'tools.verify', '-c', 'strict'], check=True)\n",
82
+ ),
83
+ ),
84
+ focus_path=PurePosixPath("tools/check.py"),
85
+ expected_count=0,
86
+ public=True,
87
+ ),
88
+ ),
89
+ )
90
+ description = documentation.summary
91
+
92
+ @override
93
+ def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
94
+ if context.generated or context.tree is None:
95
+ return []
96
+ imports = context.imports
97
+ replaced = {
98
+ name
99
+ for node in context.nodes(ast.Attribute)
100
+ if isinstance(node.ctx, (ast.Store, ast.Del))
101
+ and (name := imports.resolved_qualified_name(node)) is not None
102
+ }
103
+ findings: list[Diagnostic] = []
104
+ for call in context.nodes(ast.Call):
105
+ sink = imports.resolved_qualified_name(call.func)
106
+ if sink is None or sink in replaced or is_suppressed(context.source_lines, call.lineno, self.code):
107
+ continue
108
+ argv = _literal_argv(call, sink)
109
+ if argv is None or classify_interpreter(argv).kind != "inline":
110
+ continue
111
+ findings.append(
112
+ Diagnostic(
113
+ path=context.path,
114
+ line=call.lineno,
115
+ col=call.col_offset + 1,
116
+ code=self.code,
117
+ message="Interpreter source is embedded in process argv; invoke a linted repository-owned file or module instead.",
118
+ severity=Severity.ERROR,
119
+ )
120
+ )
121
+ return findings
122
+
123
+
124
+ def _literal_argv(call: ast.Call, sink: str) -> tuple[str, ...] | None:
125
+ if any(keyword.arg is None for keyword in call.keywords):
126
+ return None # Expanded options can replace the executable or enable shell=True.
127
+ projected = _process_arguments(call, sink)
128
+ if projected is None:
129
+ return None
130
+ argv = _vector(projected.argv)
131
+ if not argv:
132
+ return None
133
+ return _override_executable(argv, projected.executable)
134
+
135
+
136
+ def _override_executable(argv: tuple[str, ...], executable: ast.expr | None) -> tuple[str, ...] | None:
137
+ if executable is None:
138
+ return argv
139
+ resolved = _string(executable)
140
+ return None if resolved is None else (resolved, *argv[1:])
141
+
142
+
143
+ def _process_arguments(call: ast.Call, sink: str) -> ProcessArguments | None:
144
+ if sink in _SUBPROCESS:
145
+ return _subprocess_arguments(call)
146
+ if sink == "asyncio.create_subprocess_exec":
147
+ return _asyncio_arguments(call)
148
+ if sink in _EXEC_VECTOR | _SPAWN_VECTOR:
149
+ offset = 1 if sink in _SPAWN_VECTOR else 0
150
+ return ProcessArguments(
151
+ _argument(call, "argv" if "posix_spawn" in sink else "args", offset + 1), _argument(call, "path", offset)
152
+ )
153
+ if sink in _EXEC_LIST | _SPAWN_LIST:
154
+ offset = 1 if sink in _SPAWN_LIST else 0
155
+ arguments = call.args[offset + 1 : -1] if sink.endswith("e") else call.args[offset + 1 :]
156
+ return ProcessArguments(ast.Tuple(elts=arguments, ctx=ast.Load()), _argument(call, "path", offset))
157
+ return None
158
+
159
+
160
+ def _subprocess_arguments(call: ast.Call) -> ProcessArguments | None:
161
+ shell = _argument(call, "shell", len(call.args))
162
+ if shell is not None and not (isinstance(shell, ast.Constant) and shell.value is False):
163
+ return None
164
+ return ProcessArguments(_argument(call, "args", 0), _argument(call, "executable", len(call.args)))
165
+
166
+
167
+ def _asyncio_arguments(call: ast.Call) -> ProcessArguments:
168
+ arguments = call.args
169
+ if not arguments:
170
+ program = _argument(call, "program", 0)
171
+ arguments = [program] if program is not None else []
172
+ return ProcessArguments(ast.Tuple(elts=arguments, ctx=ast.Load()), _argument(call, "executable", len(call.args)))
173
+
174
+
175
+ def _argument(call: ast.Call, name: str, index: int) -> ast.expr | None:
176
+ return (
177
+ call.args[index]
178
+ if index < len(call.args)
179
+ else next((keyword.value for keyword in call.keywords if keyword.arg == name), None)
180
+ )
181
+
182
+
183
+ def _vector(node: ast.expr | None) -> tuple[str, ...] | None:
184
+ if not isinstance(node, (ast.List, ast.Tuple)):
185
+ return None
186
+ words = tuple(_string(item) for item in node.elts)
187
+ return tuple(word for word in words if word is not None) if all(word is not None for word in words) else None
188
+
189
+
190
+ def _string(node: ast.expr | None) -> str | None:
191
+ return node.value if isinstance(node, ast.Constant) and isinstance(node.value, str) else None