sarj-python-lint 0.87.0__tar.gz → 0.87.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/.gitignore +4 -3
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/PKG-INFO +1 -1
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/pyproject.toml +1 -1
- sarj_python_lint-0.87.2/src/sarj_python_lint/rules/_ast_position.py +21 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +12 -6
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +3 -2
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +15 -7
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_unused_value_marker.py +68 -8
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +3 -2
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +14 -7
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +13 -6
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +3 -2
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +16 -8
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +11 -4
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_str_enum.py +12 -5
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/require_port_for_service.py +26 -20
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +81 -44
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +31 -16
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +15 -7
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/LICENSE +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/README.md +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/require_typed_http_test_response.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
|
@@ -32,9 +32,10 @@ Thumbs.db
|
|
|
32
32
|
.zed/
|
|
33
33
|
*.swp
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
.claude/
|
|
36
|
+
apps/docs/.wrangler/
|
|
37
|
+
apps/docs/.dev.vars*
|
|
38
|
+
!apps/docs/.dev.vars.example
|
|
38
39
|
.worktrees/
|
|
39
40
|
.sarj-private-refs.toml
|
|
40
41
|
.sarj-standards-rollout.toml
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 0.87.
|
|
3
|
+
Version: 0.87.2
|
|
4
4
|
Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
|
|
5
5
|
Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
|
|
6
6
|
Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
import ast
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, order=True, slots=True)
|
|
12
|
+
class AstPosition:
|
|
13
|
+
line: int
|
|
14
|
+
column: int
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def ast_position(node: ast.AST, *, missing: int) -> AstPosition:
|
|
18
|
+
return AstPosition(
|
|
19
|
+
line=getattr(node, "lineno", missing),
|
|
20
|
+
column=getattr(node, "col_offset", missing),
|
|
21
|
+
)
|
|
@@ -674,12 +674,12 @@ def _check_direct_responses(
|
|
|
674
674
|
routes: tuple[Route, ...],
|
|
675
675
|
index: FastapiIndex,
|
|
676
676
|
) -> list[_Finding]:
|
|
677
|
-
|
|
677
|
+
direct = _direct_response_statuses(function, index)
|
|
678
678
|
findings = [
|
|
679
679
|
_Finding(node, f"[status] {status} is outside the HTTP status range 100..599.")
|
|
680
|
-
for status, node in invalid.items()
|
|
680
|
+
for status, node in direct.invalid.items()
|
|
681
681
|
]
|
|
682
|
-
if not statuses:
|
|
682
|
+
if not direct.statuses:
|
|
683
683
|
return findings
|
|
684
684
|
for route in _operations(routes):
|
|
685
685
|
responses = _route_response_nodes(route)
|
|
@@ -689,7 +689,7 @@ def _check_direct_responses(
|
|
|
689
689
|
status for node in responses if isinstance(node, ast.Dict) for status in _response_codes(node, index)
|
|
690
690
|
}
|
|
691
691
|
primary = _status_code(route.keywords.get("status_code"), index)
|
|
692
|
-
missing = sorted(statuses - documented - ({primary} if primary is not None else set()))
|
|
692
|
+
missing = sorted(direct.statuses - documented - ({primary} if primary is not None else set()))
|
|
693
693
|
if missing:
|
|
694
694
|
findings.append(
|
|
695
695
|
_Finding(
|
|
@@ -700,9 +700,15 @@ def _check_direct_responses(
|
|
|
700
700
|
return findings
|
|
701
701
|
|
|
702
702
|
|
|
703
|
+
@dataclass(frozen=True, slots=True)
|
|
704
|
+
class _DirectResponseStatuses:
|
|
705
|
+
statuses: set[int]
|
|
706
|
+
invalid: dict[int, ast.Call]
|
|
707
|
+
|
|
708
|
+
|
|
703
709
|
def _direct_response_statuses(
|
|
704
710
|
function: ast.FunctionDef | ast.AsyncFunctionDef, index: FastapiIndex
|
|
705
|
-
) ->
|
|
711
|
+
) -> _DirectResponseStatuses:
|
|
706
712
|
statuses: set[int] = set()
|
|
707
713
|
invalid: dict[int, ast.Call] = {}
|
|
708
714
|
stack: list[tuple[ast.AST, bool]] = [(node, False) for node in function.body]
|
|
@@ -721,7 +727,7 @@ def _direct_response_statuses(
|
|
|
721
727
|
else:
|
|
722
728
|
invalid.setdefault(status, call)
|
|
723
729
|
stack.extend((child, catches_http_exception) for child in children(current))
|
|
724
|
-
return statuses, invalid
|
|
730
|
+
return _DirectResponseStatuses(statuses=statuses, invalid=invalid)
|
|
725
731
|
|
|
726
732
|
|
|
727
733
|
def _extend_response_try(
|
|
@@ -20,6 +20,7 @@ from sarj_python_lint.rule_base import (
|
|
|
20
20
|
is_suppressed,
|
|
21
21
|
parse_or_none,
|
|
22
22
|
)
|
|
23
|
+
from sarj_python_lint.rules._ast_position import AstPosition, ast_position
|
|
23
24
|
from sarj_python_lint.rules._imports import ImportIndex
|
|
24
25
|
from sarj_python_lint.rules._paths import is_generated, is_test_path, is_test_support_path
|
|
25
26
|
|
|
@@ -954,8 +955,8 @@ def _target_names(node: ast.expr) -> set[str]:
|
|
|
954
955
|
return set()
|
|
955
956
|
|
|
956
957
|
|
|
957
|
-
def _position(node: ast.AST) ->
|
|
958
|
-
return
|
|
958
|
+
def _position(node: ast.AST) -> AstPosition:
|
|
959
|
+
return ast_position(node, missing=-1)
|
|
959
960
|
|
|
960
961
|
|
|
961
962
|
def _scope_nodes(statements: Iterable[ast.stmt], *, stop_rebinding: str | None = None) -> Iterator[ast.AST]:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
4
5
|
from pathlib import PurePosixPath
|
|
5
6
|
import re
|
|
6
7
|
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
@@ -26,6 +27,13 @@ if TYPE_CHECKING:
|
|
|
26
27
|
|
|
27
28
|
_RANDOM_UUID_RE = re.compile(r"\b(?:gen_random_uuid|uuid_generate_v4)\s*\(", re.IGNORECASE)
|
|
28
29
|
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class _IdentifierEnd:
|
|
33
|
+
end: int
|
|
34
|
+
after_whitespace: int
|
|
35
|
+
|
|
36
|
+
|
|
29
37
|
# Require SQL structure so prose that merely names the function stays valid.
|
|
30
38
|
_SQL_SHAPE_RE = re.compile(
|
|
31
39
|
r"\b(?:CREATE|ALTER|INSERT|UPDATE|SELECT|DEFAULT|VALUES)\b",
|
|
@@ -132,12 +140,12 @@ def _inside_uuidv7_builder(sql: str, position: int) -> bool:
|
|
|
132
140
|
index = 0
|
|
133
141
|
while index < position:
|
|
134
142
|
if sql[index].isalpha() or sql[index] == "_":
|
|
135
|
-
|
|
136
|
-
if
|
|
137
|
-
calls.append(sql[index:end].lower())
|
|
138
|
-
index =
|
|
143
|
+
identifier = _identifier_end(sql, index, position)
|
|
144
|
+
if identifier.after_whitespace < position and sql[identifier.after_whitespace] == "(":
|
|
145
|
+
calls.append(sql[index : identifier.end].lower())
|
|
146
|
+
index = identifier.after_whitespace + 1
|
|
139
147
|
continue
|
|
140
|
-
index = end
|
|
148
|
+
index = identifier.end
|
|
141
149
|
continue
|
|
142
150
|
if sql[index] == "(":
|
|
143
151
|
calls.append(None)
|
|
@@ -147,11 +155,11 @@ def _inside_uuidv7_builder(sql: str, position: int) -> bool:
|
|
|
147
155
|
return any(call in _UUIDV7_BUILDERS for call in calls)
|
|
148
156
|
|
|
149
157
|
|
|
150
|
-
def _identifier_end(sql: str, index: int, position: int) ->
|
|
158
|
+
def _identifier_end(sql: str, index: int, position: int) -> _IdentifierEnd:
|
|
151
159
|
end = index + 1
|
|
152
160
|
while end < position and (sql[end].isalnum() or sql[end] == "_"):
|
|
153
161
|
end += 1
|
|
154
162
|
after = end
|
|
155
163
|
while after < position and sql[after].isspace():
|
|
156
164
|
after += 1
|
|
157
|
-
return end, after
|
|
165
|
+
return _IdentifierEnd(end=end, after_whitespace=after)
|
|
@@ -32,7 +32,8 @@ class NoUnusedValueMarker(Rule):
|
|
|
32
32
|
summary="Do not use standalone assignments to `_` to discard values.",
|
|
33
33
|
rationale=(
|
|
34
34
|
"A standalone `_ = value` assignment adds a binding without clarifying intent. A side-effecting call can "
|
|
35
|
-
"be invoked directly
|
|
35
|
+
"be invoked directly. Pure references to formal parameters are excluded because callback and protocol "
|
|
36
|
+
"signatures may require names that Ruff otherwise reports as unused."
|
|
36
37
|
),
|
|
37
38
|
remediation=(
|
|
38
39
|
"Invoke calls directly instead of assigning their result to `_`. Remove or rename locally owned unused "
|
|
@@ -43,14 +44,15 @@ class NoUnusedValueMarker(Rule):
|
|
|
43
44
|
autofix=AutofixPolicy.NONE,
|
|
44
45
|
limitations=(
|
|
45
46
|
"Plain and annotated assignments whose standalone target is `_` are reported, including chained assignments.",
|
|
47
|
+
"Pure name or tuple references to the enclosing function's unchanged parameters are excluded.",
|
|
46
48
|
"Unpacking placeholders, loop targets, wildcard imports, and underscore-prefixed names are excluded.",
|
|
47
49
|
"Generated and vendored files are excluded.",
|
|
48
50
|
),
|
|
49
51
|
examples=(
|
|
50
52
|
RuleExample(
|
|
51
|
-
example_id="
|
|
52
|
-
title="
|
|
53
|
-
outcome=ExampleOutcome.
|
|
53
|
+
example_id="callback-parameter-marker",
|
|
54
|
+
title="A callback keeps its required parameter names",
|
|
55
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
54
56
|
files=(
|
|
55
57
|
ExampleFile.python(
|
|
56
58
|
"app/adapter.py",
|
|
@@ -58,6 +60,15 @@ class NoUnusedValueMarker(Rule):
|
|
|
58
60
|
),
|
|
59
61
|
),
|
|
60
62
|
focus_path=PurePosixPath("app/adapter.py"),
|
|
63
|
+
expected_count=0,
|
|
64
|
+
public=False,
|
|
65
|
+
),
|
|
66
|
+
RuleExample(
|
|
67
|
+
example_id="discarded-nonparameter-value",
|
|
68
|
+
title="A standalone discard binding is unnecessary",
|
|
69
|
+
outcome=ExampleOutcome.MATCH,
|
|
70
|
+
files=(ExampleFile.python("app/adapter.py", "value = 1\n_ = value\n"),),
|
|
71
|
+
focus_path=PurePosixPath("app/adapter.py"),
|
|
61
72
|
expected_count=1,
|
|
62
73
|
public=True,
|
|
63
74
|
),
|
|
@@ -116,6 +127,18 @@ class NoUnusedValueMarker(Rule):
|
|
|
116
127
|
if tree is None:
|
|
117
128
|
return []
|
|
118
129
|
source_lines = source.splitlines()
|
|
130
|
+
markers = [
|
|
131
|
+
node
|
|
132
|
+
for node in ast.walk(tree)
|
|
133
|
+
if isinstance(node, (ast.Assign, ast.AnnAssign)) and _is_unused_value_marker(node)
|
|
134
|
+
]
|
|
135
|
+
if not markers:
|
|
136
|
+
return []
|
|
137
|
+
parents = (
|
|
138
|
+
{child: parent for parent in ast.walk(tree) for child in ast.iter_child_nodes(parent)}
|
|
139
|
+
if any(isinstance(node, ast.Assign) and isinstance(node.value, (ast.Name, ast.Tuple)) for node in markers)
|
|
140
|
+
else {}
|
|
141
|
+
)
|
|
119
142
|
return [
|
|
120
143
|
Diagnostic(
|
|
121
144
|
path=path,
|
|
@@ -128,10 +151,8 @@ class NoUnusedValueMarker(Rule):
|
|
|
128
151
|
"directly or address unused values at their declaration."
|
|
129
152
|
),
|
|
130
153
|
)
|
|
131
|
-
for node in
|
|
132
|
-
if
|
|
133
|
-
and _is_unused_value_marker(node)
|
|
134
|
-
and not is_suppressed(source_lines, node.lineno, self.code)
|
|
154
|
+
for node in markers
|
|
155
|
+
if not _is_pure_parameter_marker(node, parents) and not is_suppressed(source_lines, node.lineno, self.code)
|
|
135
156
|
]
|
|
136
157
|
|
|
137
158
|
|
|
@@ -139,3 +160,42 @@ def _is_unused_value_marker(node: ast.Assign | ast.AnnAssign) -> bool:
|
|
|
139
160
|
if isinstance(node, ast.AnnAssign):
|
|
140
161
|
return node.value is not None and isinstance(node.target, ast.Name) and node.target.id == "_"
|
|
141
162
|
return any(isinstance(target, ast.Name) and target.id == "_" for target in node.targets)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _is_pure_parameter_marker(
|
|
166
|
+
node: ast.Assign | ast.AnnAssign,
|
|
167
|
+
parents: dict[ast.AST, ast.AST],
|
|
168
|
+
) -> bool:
|
|
169
|
+
if not isinstance(node, ast.Assign) or len(node.targets) != 1:
|
|
170
|
+
return False
|
|
171
|
+
if not isinstance(node.targets[0], ast.Name) or node.targets[0].id != "_":
|
|
172
|
+
return False
|
|
173
|
+
names = _marker_reference_names(node.value)
|
|
174
|
+
if names is None:
|
|
175
|
+
return False
|
|
176
|
+
function = _enclosing_function(node, parents)
|
|
177
|
+
if function is None:
|
|
178
|
+
return False
|
|
179
|
+
parameters = {argument.arg for argument in ast.walk(function.args) if isinstance(argument, ast.arg)}
|
|
180
|
+
return names <= parameters and not any(
|
|
181
|
+
isinstance(child, ast.Name) and isinstance(child.ctx, ast.Store) and child.id in names
|
|
182
|
+
for child in ast.walk(function)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _marker_reference_names(value: ast.expr) -> set[str] | None:
|
|
187
|
+
values = value.elts if isinstance(value, ast.Tuple) else [value]
|
|
188
|
+
if not values or not all(isinstance(item, ast.Name) for item in values):
|
|
189
|
+
return None
|
|
190
|
+
return {item.id for item in values if isinstance(item, ast.Name)}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _enclosing_function(
|
|
194
|
+
node: ast.AST, parents: dict[ast.AST, ast.AST]
|
|
195
|
+
) -> ast.FunctionDef | ast.AsyncFunctionDef | None:
|
|
196
|
+
current = node
|
|
197
|
+
while current in parents:
|
|
198
|
+
current = parents[current]
|
|
199
|
+
if isinstance(current, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
200
|
+
return current
|
|
201
|
+
return None
|
|
@@ -16,6 +16,7 @@ from sarj_python_lint.rule_base import (
|
|
|
16
16
|
Severity,
|
|
17
17
|
parse_or_none,
|
|
18
18
|
)
|
|
19
|
+
from sarj_python_lint.rules._ast_position import AstPosition, ast_position
|
|
19
20
|
from sarj_python_lint.rules._imports import ImportIndex
|
|
20
21
|
from sarj_python_lint.rules._paths import is_generated, is_test_path
|
|
21
22
|
|
|
@@ -250,10 +251,10 @@ def _parameter_rebound_before(
|
|
|
250
251
|
return any(handle in _bound_targets(node) and _position(node) < use_position for node in nodes)
|
|
251
252
|
|
|
252
253
|
|
|
253
|
-
def _position(node: ast.AST) ->
|
|
254
|
+
def _position(node: ast.AST) -> AstPosition:
|
|
254
255
|
if isinstance(node, ast.withitem):
|
|
255
256
|
return _position(node.context_expr)
|
|
256
|
-
return (
|
|
257
|
+
return ast_position(node, missing=0)
|
|
257
258
|
|
|
258
259
|
|
|
259
260
|
def _direct_aliases(nodes: tuple[ast.AST, ...], handles: set[str], parameters: set[str]) -> set[str]:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
4
5
|
from pathlib import PurePosixPath
|
|
5
6
|
from typing import TYPE_CHECKING, ClassVar, NamedTuple, final, override
|
|
6
7
|
|
|
@@ -215,17 +216,23 @@ def _classifier_starts(
|
|
|
215
216
|
for statements in _nested_statement_lists(body):
|
|
216
217
|
index = 0
|
|
217
218
|
while index < len(statements):
|
|
218
|
-
|
|
219
|
-
branches = _validated_branches(candidates, subject, imports)
|
|
220
|
-
if branches is not None and _is_classifier(branches, statements, cursor):
|
|
219
|
+
adjacent = _adjacent_bare_ifs(statements, index)
|
|
220
|
+
branches = _validated_branches(adjacent.candidates, subject, imports)
|
|
221
|
+
if branches is not None and _is_classifier(branches, statements, adjacent.cursor):
|
|
221
222
|
findings.append(branches[0][0])
|
|
222
|
-
index = cursor + 1
|
|
223
|
+
index = adjacent.cursor + 1
|
|
223
224
|
else:
|
|
224
|
-
index = max(cursor, index + 1)
|
|
225
|
+
index = max(adjacent.cursor, index + 1)
|
|
225
226
|
return tuple(findings)
|
|
226
227
|
|
|
227
228
|
|
|
228
|
-
|
|
229
|
+
@dataclass(frozen=True, slots=True)
|
|
230
|
+
class _AdjacentBareIfs:
|
|
231
|
+
candidates: list[ast.If]
|
|
232
|
+
cursor: int
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _adjacent_bare_ifs(statements: list[ast.stmt], start: int) -> _AdjacentBareIfs:
|
|
229
236
|
cursor = start
|
|
230
237
|
candidates: list[ast.If] = []
|
|
231
238
|
while cursor < len(statements):
|
|
@@ -234,7 +241,7 @@ def _adjacent_bare_ifs(statements: list[ast.stmt], start: int) -> tuple[list[ast
|
|
|
234
241
|
break
|
|
235
242
|
candidates.append(candidate)
|
|
236
243
|
cursor += 1
|
|
237
|
-
return candidates, cursor
|
|
244
|
+
return _AdjacentBareIfs(candidates=candidates, cursor=cursor)
|
|
238
245
|
|
|
239
246
|
|
|
240
247
|
def _validated_branches(
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
4
|
from collections import Counter
|
|
5
|
+
from dataclasses import dataclass
|
|
5
6
|
from pathlib import PurePosixPath
|
|
6
7
|
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
7
8
|
|
|
@@ -164,12 +165,12 @@ class PreferMatchValueDispatch(Rule):
|
|
|
164
165
|
for node in ast.walk(tree):
|
|
165
166
|
if not isinstance(node, ast.If) or id(node) in continuations:
|
|
166
167
|
continue
|
|
167
|
-
|
|
168
|
-
if len(branches) < _MIN_TESTS or not
|
|
168
|
+
chain = _elif_chain(node, lines, continuations)
|
|
169
|
+
if len(chain.branches) < _MIN_TESTS or not chain.last.orelse:
|
|
169
170
|
continue
|
|
170
|
-
subject = _dispatch_subject(branches, constants)
|
|
171
|
+
subject = _dispatch_subject(chain.branches, constants)
|
|
171
172
|
if subject is None:
|
|
172
|
-
subject = _expanded_dispatch_subject(branches, constants)
|
|
173
|
+
subject = _expanded_dispatch_subject(chain.branches, constants)
|
|
173
174
|
if subject is not None:
|
|
174
175
|
candidates.append((node, subject))
|
|
175
176
|
for branches in _terminal_sibling_dispatches(tree):
|
|
@@ -368,7 +369,13 @@ def _subject(expression: ast.expr) -> str | None:
|
|
|
368
369
|
return None
|
|
369
370
|
|
|
370
371
|
|
|
371
|
-
|
|
372
|
+
@dataclass(frozen=True, slots=True)
|
|
373
|
+
class _ElifChain:
|
|
374
|
+
branches: list[ast.If]
|
|
375
|
+
last: ast.If
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _elif_chain(node: ast.If, lines: list[str], continuations: set[int]) -> _ElifChain:
|
|
372
379
|
branches = [node]
|
|
373
380
|
current = node
|
|
374
381
|
while len(current.orelse) == 1:
|
|
@@ -378,7 +385,7 @@ def _elif_chain(node: ast.If, lines: list[str], continuations: set[int]) -> tupl
|
|
|
378
385
|
continuations.add(id(child))
|
|
379
386
|
branches.append(child)
|
|
380
387
|
current = child
|
|
381
|
-
return branches, current
|
|
388
|
+
return _ElifChain(branches=branches, last=current)
|
|
382
389
|
|
|
383
390
|
|
|
384
391
|
def _collect_terminal_siblings(field: list[ast.stmt], candidates: list[list[ast.If]]) -> None:
|
|
@@ -16,6 +16,7 @@ from sarj_python_lint.rule_base import (
|
|
|
16
16
|
Severity,
|
|
17
17
|
parse_or_none,
|
|
18
18
|
)
|
|
19
|
+
from sarj_python_lint.rules._ast_position import AstPosition, ast_position
|
|
19
20
|
from sarj_python_lint.rules._imports import ImportIndex
|
|
20
21
|
from sarj_python_lint.rules._paths import is_generated, is_test_path
|
|
21
22
|
|
|
@@ -294,8 +295,8 @@ def _import_root_rebound_before(
|
|
|
294
295
|
)
|
|
295
296
|
|
|
296
297
|
|
|
297
|
-
def _position(node: ast.AST) ->
|
|
298
|
-
return (
|
|
298
|
+
def _position(node: ast.AST) -> AstPosition:
|
|
299
|
+
return ast_position(node, missing=0)
|
|
299
300
|
|
|
300
301
|
|
|
301
302
|
def _restored_mutations(
|
|
@@ -377,9 +377,11 @@ def _collect_type_aliases(
|
|
|
377
377
|
aliases: dict[str, ast.expr],
|
|
378
378
|
) -> None:
|
|
379
379
|
for statement in tree.body:
|
|
380
|
-
|
|
381
|
-
if
|
|
380
|
+
assignment = _alias_assignment(statement)
|
|
381
|
+
if assignment is None:
|
|
382
382
|
continue
|
|
383
|
+
target = assignment.target
|
|
384
|
+
value = assignment.value
|
|
383
385
|
if _is_new_type_call(value, imports):
|
|
384
386
|
if len(value.args) >= _MIN_SWAPPABLE_ROLES:
|
|
385
387
|
carrier = _carrier(value.args[_SECOND_ARGUMENT], imports, _TypeFacts(raw_aliases, nominal_aliases))
|
|
@@ -389,14 +391,20 @@ def _collect_type_aliases(
|
|
|
389
391
|
aliases[target] = _type_alias_type_value(value, imports) or value
|
|
390
392
|
|
|
391
393
|
|
|
392
|
-
|
|
394
|
+
@dataclass(frozen=True, slots=True)
|
|
395
|
+
class _AliasAssignment:
|
|
396
|
+
target: str
|
|
397
|
+
value: ast.expr
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _alias_assignment(statement: ast.stmt) -> _AliasAssignment | None:
|
|
393
401
|
if isinstance(statement, ast.TypeAlias):
|
|
394
|
-
return statement.name.id, statement.value
|
|
402
|
+
return _AliasAssignment(target=statement.name.id, value=statement.value)
|
|
395
403
|
if isinstance(statement, ast.Assign) and len(statement.targets) == 1 and isinstance(statement.targets[0], ast.Name):
|
|
396
|
-
return statement.targets[0].id, statement.value
|
|
397
|
-
if isinstance(statement, ast.AnnAssign) and isinstance(statement.target, ast.Name):
|
|
398
|
-
return statement.target.id, statement.value
|
|
399
|
-
return None
|
|
404
|
+
return _AliasAssignment(target=statement.targets[0].id, value=statement.value)
|
|
405
|
+
if isinstance(statement, ast.AnnAssign) and isinstance(statement.target, ast.Name) and statement.value is not None:
|
|
406
|
+
return _AliasAssignment(target=statement.target.id, value=statement.value)
|
|
407
|
+
return None
|
|
400
408
|
|
|
401
409
|
|
|
402
410
|
def _is_new_type_call(value: ast.expr, imports: ImportIndex) -> TypeGuard[ast.Call]:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
4
5
|
from enum import StrEnum
|
|
5
6
|
from pathlib import PurePosixPath
|
|
6
7
|
from typing import TYPE_CHECKING, ClassVar, override
|
|
@@ -237,8 +238,8 @@ class PreferSelfTypeAnnotation(Rule):
|
|
|
237
238
|
if method_kind is None or method_kind is _MethodKind.STATIC:
|
|
238
239
|
return
|
|
239
240
|
receiver = _first_positional_parameter(node)
|
|
240
|
-
|
|
241
|
-
if receiver is None or receiver in bindings or is_generator:
|
|
241
|
+
scope = _method_scope_facts(node)
|
|
242
|
+
if receiver is None or receiver in scope.bindings or scope.is_generator:
|
|
242
243
|
return
|
|
243
244
|
returns = node.returns
|
|
244
245
|
if returns is None:
|
|
@@ -337,7 +338,13 @@ def _matched_enclosing_class_annotation(node: ast.expr, class_name: str) -> str
|
|
|
337
338
|
return None
|
|
338
339
|
|
|
339
340
|
|
|
340
|
-
|
|
341
|
+
@dataclass(frozen=True, slots=True)
|
|
342
|
+
class _MethodScopeFacts:
|
|
343
|
+
bindings: frozenset[str]
|
|
344
|
+
is_generator: bool
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _method_scope_facts(node: ast.FunctionDef | ast.AsyncFunctionDef) -> _MethodScopeFacts:
|
|
341
348
|
bindings: set[str] = set()
|
|
342
349
|
has_yield = False
|
|
343
350
|
|
|
@@ -409,7 +416,7 @@ def _method_scope_facts(node: ast.FunctionDef | ast.AsyncFunctionDef) -> tuple[f
|
|
|
409
416
|
visitor = Visitor()
|
|
410
417
|
for statement in node.body:
|
|
411
418
|
visitor.visit(statement)
|
|
412
|
-
return frozenset(bindings), has_yield
|
|
419
|
+
return _MethodScopeFacts(bindings=frozenset(bindings), is_generator=has_yield)
|
|
413
420
|
|
|
414
421
|
|
|
415
422
|
def _is_final_class(node: ast.ClassDef, imports: ImportIndex) -> bool:
|
{sarj_python_lint-0.87.0 → sarj_python_lint-0.87.2}/src/sarj_python_lint/rules/prefer_str_enum.py
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
4
5
|
from pathlib import PurePosixPath
|
|
5
6
|
import re
|
|
6
7
|
from types import MappingProxyType
|
|
@@ -499,9 +500,9 @@ class PreferStrEnum(Rule):
|
|
|
499
500
|
choice_groups = _class_choice_groups(cls)
|
|
500
501
|
candidates = _class_choice_fields(cls, raw_string_aliases, imports)
|
|
501
502
|
for stmt, name, default in candidates:
|
|
502
|
-
|
|
503
|
-
if not
|
|
504
|
-
default is not None and len(candidates) == 1 and default in
|
|
503
|
+
choices = _choice_values_for_field(choice_groups, name)
|
|
504
|
+
if not choices.associated and not (
|
|
505
|
+
default is not None and len(candidates) == 1 and default in choices.generic
|
|
505
506
|
):
|
|
506
507
|
continue
|
|
507
508
|
diags.append(
|
|
@@ -1429,14 +1430,20 @@ def _match_pattern_literals(pattern: ast.pattern) -> list[str]:
|
|
|
1429
1430
|
return []
|
|
1430
1431
|
|
|
1431
1432
|
|
|
1432
|
-
|
|
1433
|
+
@dataclass(frozen=True, slots=True)
|
|
1434
|
+
class _ChoiceValues:
|
|
1435
|
+
associated: set[str]
|
|
1436
|
+
generic: set[str]
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
def _choice_values_for_field(choice_groups: list[tuple[str, set[str]]], name: str) -> _ChoiceValues:
|
|
1433
1440
|
associated_values = {
|
|
1434
1441
|
value for binding, values in choice_groups if _choice_binding_field(binding) == name.lower() for value in values
|
|
1435
1442
|
}
|
|
1436
1443
|
generic_values = {
|
|
1437
1444
|
value for binding, values in choice_groups if _choice_binding_field(binding) is None for value in values
|
|
1438
1445
|
}
|
|
1439
|
-
return associated_values, generic_values
|
|
1446
|
+
return _ChoiceValues(associated=associated_values, generic=generic_values)
|
|
1440
1447
|
|
|
1441
1448
|
|
|
1442
1449
|
def _choice_binding_field(binding: str) -> str | None:
|
|
@@ -703,29 +703,35 @@ def _annotation_allows_none(annotation: ast.expr | None) -> bool:
|
|
|
703
703
|
return isinstance(annotation, ast.Subscript) and _subscript_allows_none(annotation)
|
|
704
704
|
|
|
705
705
|
|
|
706
|
-
|
|
706
|
+
@dataclass(frozen=True, slots=True)
|
|
707
|
+
class _InvocationFlow:
|
|
708
|
+
found: bool
|
|
709
|
+
falls_through: bool
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _statements_invoke_field(statements: list[ast.stmt], fields: frozenset[str]) -> _InvocationFlow:
|
|
707
713
|
for statement in statements:
|
|
708
714
|
if isinstance(statement, ast.If):
|
|
709
|
-
|
|
710
|
-
if found or not falls_through:
|
|
711
|
-
return
|
|
715
|
+
flow = _conditional_invokes_field(statement, fields)
|
|
716
|
+
if flow.found or not flow.falls_through:
|
|
717
|
+
return flow
|
|
712
718
|
continue
|
|
713
719
|
if (
|
|
714
720
|
isinstance(statement, ast.While)
|
|
715
721
|
and isinstance(statement.test, ast.Constant)
|
|
716
722
|
and not bool(statement.test.value)
|
|
717
723
|
):
|
|
718
|
-
|
|
719
|
-
if found or not falls_through:
|
|
720
|
-
return
|
|
724
|
+
flow = _statements_invoke_field(statement.orelse, fields)
|
|
725
|
+
if flow.found or not flow.falls_through:
|
|
726
|
+
return flow
|
|
721
727
|
continue
|
|
722
728
|
if isinstance(statement, (ast.While, *_UNSUPPORTED_COMPOUND_STATEMENTS)):
|
|
723
729
|
continue
|
|
724
730
|
if _node_invokes_field(statement, fields):
|
|
725
|
-
return True, True
|
|
731
|
+
return _InvocationFlow(found=True, falls_through=True)
|
|
726
732
|
if isinstance(statement, ast.Return | ast.Raise):
|
|
727
|
-
return False, False
|
|
728
|
-
return False, True
|
|
733
|
+
return _InvocationFlow(found=False, falls_through=False)
|
|
734
|
+
return _InvocationFlow(found=False, falls_through=True)
|
|
729
735
|
|
|
730
736
|
|
|
731
737
|
def _node_invokes_field(node: ast.AST, fields: frozenset[str]) -> bool:
|
|
@@ -881,8 +887,7 @@ def _method_invokes_field(
|
|
|
881
887
|
) -> bool:
|
|
882
888
|
if _has_unsupported_control_flow(method.body):
|
|
883
889
|
return False
|
|
884
|
-
|
|
885
|
-
return found
|
|
890
|
+
return _statements_invoke_field(method.body, fields).found
|
|
886
891
|
|
|
887
892
|
|
|
888
893
|
def _has_unsupported_control_flow(statements: list[ast.stmt]) -> bool:
|
|
@@ -945,17 +950,18 @@ def _constant_annotation_allows_none(annotation: ast.Constant) -> bool:
|
|
|
945
950
|
return False
|
|
946
951
|
|
|
947
952
|
|
|
948
|
-
def _conditional_invokes_field(statement: ast.If, fields: frozenset[str]) ->
|
|
953
|
+
def _conditional_invokes_field(statement: ast.If, fields: frozenset[str]) -> _InvocationFlow:
|
|
949
954
|
if _node_invokes_field(statement.test, fields):
|
|
950
|
-
return True, True
|
|
955
|
+
return _InvocationFlow(found=True, falls_through=True)
|
|
951
956
|
if isinstance(statement.test, ast.Constant):
|
|
952
957
|
branch = statement.body if bool(statement.test.value) else statement.orelse
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
found
|
|
958
|
-
|
|
958
|
+
return _statements_invoke_field(branch, fields)
|
|
959
|
+
body = _statements_invoke_field(statement.body, fields)
|
|
960
|
+
otherwise = _statements_invoke_field(statement.orelse, fields)
|
|
961
|
+
return _InvocationFlow(
|
|
962
|
+
found=body.found or otherwise.found,
|
|
963
|
+
falls_through=body.falls_through or otherwise.falls_through,
|
|
964
|
+
)
|
|
959
965
|
|
|
960
966
|
|
|
961
967
|
def _boolean_invokes_field(node: ast.BoolOp, fields: frozenset[str]) -> bool:
|