sarj-python-lint 0.88.0__tar.gz → 1.1.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-0.88.0 → sarj_python_lint-1.1.0}/.gitignore +1 -1
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/PKG-INFO +2 -1
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/pyproject.toml +5 -2
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/__main__.py +11 -6
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/_analysis_session.py +6 -1
- sarj_python_lint-1.1.0/src/sarj_python_lint/_file_context.py +68 -0
- sarj_python_lint-1.1.0/src/sarj_python_lint/benchmark.py +118 -0
- sarj_python_lint-1.1.0/src/sarj_python_lint/rule_base.py +158 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_ast_index.py +10 -13
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_fastapi.py +4 -3
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_fixed_record.py +11 -9
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_imports.py +3 -1
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_project_index.py +4 -3
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_prose_budget.py +3 -3
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +13 -11
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +56 -35
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +8 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +8 -7
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +8 -7
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/excessive_commentary.py +10 -7
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +7 -5
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +24 -13
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +12 -12
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +8 -9
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +12 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/mock_without_spec.py +29 -13
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +17 -19
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +9 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +12 -11
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +23 -12
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_comment_cruft.py +6 -3
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +5 -6
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +6 -5
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +8 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_delete_statement.py +8 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_dunder_all.py +6 -6
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +7 -7
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +7 -7
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +7 -5
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +4 -2
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +27 -17
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +9 -7
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +9 -9
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +9 -7
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +8 -12
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +5 -4
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +12 -10
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_offset_pagination.py +10 -10
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +15 -11
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +15 -10
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +6 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +10 -7
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +13 -12
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +9 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +8 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +7 -5
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +8 -6
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_restated_comment.py +14 -14
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_secret_in_log.py +6 -5
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_select_star.py +13 -12
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +11 -9
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +7 -6
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_tautological_expect.py +13 -12
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +8 -3
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +18 -13
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +46 -24
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +12 -11
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +5 -5
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +15 -10
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +21 -13
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/over_mocked_test.py +7 -5
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/phase_label_comment.py +6 -5
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_class_row.py +16 -9
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +23 -18
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +18 -15
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +11 -11
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +6 -6
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +21 -14
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_library_fake.py +7 -5
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +17 -13
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +30 -17
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +10 -9
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +17 -16
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +25 -22
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +20 -13
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +18 -11
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +15 -11
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +17 -13
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +8 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +9 -7
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +21 -15
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +22 -19
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +14 -10
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_str_enum.py +46 -27
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +23 -14
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +13 -12
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +12 -9
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +16 -11
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +14 -10
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +10 -7
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +9 -9
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +12 -12
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/redundant_docstring.py +17 -13
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +14 -9
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +8 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +9 -7
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +31 -24
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +12 -9
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/require_port_for_service.py +31 -20
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +34 -20
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +16 -14
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +11 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/restated_test_docstring.py +20 -15
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/stepdown.py +7 -5
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +7 -7
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +13 -14
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +7 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/trailing_value_narration.py +12 -10
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/typed_error_reasons.py +11 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +7 -5
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/unused_mock_setup.py +11 -10
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +46 -24
- sarj_python_lint-0.88.0/src/sarj_python_lint/rule_base.py +0 -321
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/LICENSE +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/README.md +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.1.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/
|
|
@@ -19,6 +19,7 @@ Classifier: Topic :: Software Development :: Quality Assurance
|
|
|
19
19
|
Requires-Python: >=3.14
|
|
20
20
|
Requires-Dist: packaging==26.3
|
|
21
21
|
Requires-Dist: ruff>=0.16.8
|
|
22
|
+
Requires-Dist: sarj-rule-contracts==0.1.0
|
|
22
23
|
Requires-Dist: sqlglot==30.19.0
|
|
23
24
|
Requires-Dist: typer==0.27.2
|
|
24
25
|
Description-Content-Type: text/markdown
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sarj-python-lint"
|
|
3
|
-
version = "
|
|
3
|
+
version = "1.1.0"
|
|
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" }]
|
|
@@ -15,7 +15,7 @@ classifiers = [
|
|
|
15
15
|
"Programming Language :: Python :: 3.14",
|
|
16
16
|
"Topic :: Software Development :: Quality Assurance",
|
|
17
17
|
]
|
|
18
|
-
dependencies = ["packaging==26.3", "ruff>=0.16.8", "sqlglot==30.19.0", "typer==0.27.2"]
|
|
18
|
+
dependencies = ["sarj-rule-contracts==0.1.0", "packaging==26.3", "ruff>=0.16.8", "sqlglot==30.19.0", "typer==0.27.2"]
|
|
19
19
|
|
|
20
20
|
[project.scripts]
|
|
21
21
|
sarj-python-lint = "sarj_python_lint.__main__:main"
|
|
@@ -69,3 +69,6 @@ extend = "../../.ruff-strict.toml"
|
|
|
69
69
|
# Ruff is invoked at runtime through `sys.executable -m ruff`, which Deptry's
|
|
70
70
|
# import analysis cannot observe.
|
|
71
71
|
DEP002 = ["ruff"]
|
|
72
|
+
|
|
73
|
+
[tool.uv.sources]
|
|
74
|
+
sarj-rule-contracts = { path = "../contracts", editable = true }
|
|
@@ -13,6 +13,7 @@ import typer
|
|
|
13
13
|
|
|
14
14
|
from sarj_python_lint import __version__
|
|
15
15
|
from sarj_python_lint._analysis_session import AnalysisSession
|
|
16
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
16
17
|
from sarj_python_lint._filesystem import atomic_write_text
|
|
17
18
|
from sarj_python_lint.json_boundary import is_object_mapping, parse_json
|
|
18
19
|
from sarj_python_lint.rule_base import Diagnostic, ProjectRule, Rule, Severity, is_suppressed
|
|
@@ -47,7 +48,7 @@ SKIP_DIR_NAMES = frozenset(
|
|
|
47
48
|
_MAX_FILE_BYTES = 500_000
|
|
48
49
|
|
|
49
50
|
|
|
50
|
-
def
|
|
51
|
+
def expand_paths(paths: list[Path]) -> list[Path]:
|
|
51
52
|
out: list[Path] = []
|
|
52
53
|
for p in paths:
|
|
53
54
|
if not p.exists():
|
|
@@ -91,7 +92,7 @@ def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
|
|
|
91
92
|
for rule in rules:
|
|
92
93
|
rule.prepare_session(session)
|
|
93
94
|
clear_path_caches()
|
|
94
|
-
expanded =
|
|
95
|
+
expanded = expand_paths(paths)
|
|
95
96
|
loaded: dict[Path, str] = {}
|
|
96
97
|
for path in expanded:
|
|
97
98
|
try:
|
|
@@ -101,17 +102,21 @@ def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
|
|
|
101
102
|
project_rules = [rule for rule in rules if isinstance(rule, ProjectRule)]
|
|
102
103
|
if project_rules:
|
|
103
104
|
indexes = ProjectIndexSet.build(expanded, loaded, facts=session.first_party)
|
|
105
|
+
session.project = indexes
|
|
104
106
|
for rule in project_rules:
|
|
105
107
|
rule.prepare(indexes)
|
|
106
108
|
diags: list[Diagnostic] = []
|
|
107
109
|
for p, source in loaded.items():
|
|
108
|
-
diags.extend(
|
|
110
|
+
diags.extend(check_source(rules, p, source, session=session))
|
|
109
111
|
return diags
|
|
110
112
|
|
|
111
113
|
|
|
112
|
-
def
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
def check_source(
|
|
115
|
+
rules: list[Rule], p: Path, source: str, *, session: AnalysisSession | None = None
|
|
116
|
+
) -> list[Diagnostic]:
|
|
117
|
+
context = PythonFileContext(p, source, session)
|
|
118
|
+
source_lines = context.source_lines
|
|
119
|
+
raw = [diagnostic for rule in rules for diagnostic in rule.check_context(context)]
|
|
115
120
|
return deduplicate_diagnostics(
|
|
116
121
|
[
|
|
117
122
|
diagnostic
|
{sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/_analysis_session.py
RENAMED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import final
|
|
3
|
+
from typing import TYPE_CHECKING, final
|
|
4
4
|
|
|
5
5
|
from sarj_python_lint._python_target import PythonTargetFacts
|
|
6
6
|
from sarj_python_lint.rules._first_party import FirstPartyFacts
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from sarj_python_lint.rules._project_index import ProjectIndexSet
|
|
11
|
+
|
|
12
|
+
|
|
9
13
|
@final
|
|
10
14
|
class AnalysisSession:
|
|
11
15
|
def __init__(self) -> None:
|
|
16
|
+
self.project: ProjectIndexSet | None = None
|
|
12
17
|
self.first_party = FirstPartyFacts()
|
|
13
18
|
self.python_target = PythonTargetFacts()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
from typing import TYPE_CHECKING, final
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint._analysis_session import AnalysisSession
|
|
8
|
+
from sarj_python_lint.rules._ast_index import NodeIndex
|
|
9
|
+
from sarj_python_lint.rules._fastapi import FastapiIndex
|
|
10
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
11
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@final
|
|
19
|
+
class PythonFileContext:
|
|
20
|
+
def __init__(self, path: Path, source: str, session: AnalysisSession | None = None) -> None:
|
|
21
|
+
self.path = path
|
|
22
|
+
self.source = source
|
|
23
|
+
self.session = session if session is not None else AnalysisSession()
|
|
24
|
+
|
|
25
|
+
@cached_property
|
|
26
|
+
def tree(self) -> ast.Module | None:
|
|
27
|
+
if self.session.project is not None:
|
|
28
|
+
unit = self.session.project.unit(self.path)
|
|
29
|
+
if unit is not None and unit.source == self.source:
|
|
30
|
+
return unit.tree
|
|
31
|
+
try:
|
|
32
|
+
return ast.parse(self.source, filename=str(self.path))
|
|
33
|
+
except SyntaxError:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
@cached_property
|
|
37
|
+
def source_lines(self) -> list[str]:
|
|
38
|
+
return self.source.splitlines()
|
|
39
|
+
|
|
40
|
+
@cached_property
|
|
41
|
+
def generated(self) -> bool:
|
|
42
|
+
return is_generated(self.path, self.source)
|
|
43
|
+
|
|
44
|
+
@cached_property
|
|
45
|
+
def node_index(self) -> NodeIndex:
|
|
46
|
+
return NodeIndex(self._valid_tree())
|
|
47
|
+
|
|
48
|
+
def nodes[NodeT: ast.AST](self, *types: type[NodeT]) -> list[NodeT]:
|
|
49
|
+
return [node for node in self.node_index.query(types) if isinstance(node, types)]
|
|
50
|
+
|
|
51
|
+
@cached_property
|
|
52
|
+
def imports(self) -> ImportIndex:
|
|
53
|
+
return ImportIndex.from_tree(self._valid_tree())
|
|
54
|
+
|
|
55
|
+
@cached_property
|
|
56
|
+
def module_imports(self) -> ImportIndex:
|
|
57
|
+
return ImportIndex.from_tree(self._valid_tree(), module_scope_only=True)
|
|
58
|
+
|
|
59
|
+
@cached_property
|
|
60
|
+
def fastapi(self) -> FastapiIndex:
|
|
61
|
+
return FastapiIndex(self._valid_tree(), path=self.path)
|
|
62
|
+
|
|
63
|
+
def _valid_tree(self) -> ast.Module:
|
|
64
|
+
tree = self.tree
|
|
65
|
+
if tree is None:
|
|
66
|
+
msg = "syntax-dependent facts require a successfully parsed source file"
|
|
67
|
+
raise ValueError(msg)
|
|
68
|
+
return tree
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
from os.path import commonpath
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from statistics import median
|
|
9
|
+
import sys
|
|
10
|
+
from time import perf_counter
|
|
11
|
+
from typing import Annotated
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
|
|
15
|
+
from sarj_python_lint.__main__ import analyze, expand_paths
|
|
16
|
+
from sarj_python_lint.rules import REGISTRY
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class BenchmarkSample:
|
|
21
|
+
seconds: float
|
|
22
|
+
diagnostics: int
|
|
23
|
+
diagnostic_digest: str
|
|
24
|
+
peak_memory_bytes: int | None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class BenchmarkReport:
|
|
29
|
+
files: int
|
|
30
|
+
source_bytes: int
|
|
31
|
+
source_digest: str
|
|
32
|
+
rules: tuple[str, ...]
|
|
33
|
+
samples: tuple[BenchmarkSample, ...]
|
|
34
|
+
median_seconds: float
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def peak_memory_bytes() -> int | None:
|
|
38
|
+
try:
|
|
39
|
+
import resource # ruff: ignore[import-outside-top-level] — optional Unix-only memory instrumentation.
|
|
40
|
+
except ImportError:
|
|
41
|
+
return None
|
|
42
|
+
peak = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
|
43
|
+
return peak if sys.platform == "darwin" else peak * 1024
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def measure(paths: list[Path], rule_ids: list[str] | None = None, *, repeats: int = 5) -> BenchmarkReport:
|
|
47
|
+
if repeats < 1:
|
|
48
|
+
msg = "repeats must be positive"
|
|
49
|
+
raise ValueError(msg)
|
|
50
|
+
selected = sorted(REGISTRY) if rule_ids is None else sorted(rule_ids)
|
|
51
|
+
unknown = sorted(set(selected) - REGISTRY.keys())
|
|
52
|
+
if unknown:
|
|
53
|
+
msg = f"unknown rule(s): {', '.join(unknown)}"
|
|
54
|
+
raise ValueError(msg)
|
|
55
|
+
expanded = sorted(set(expand_paths(paths)))
|
|
56
|
+
if not expanded:
|
|
57
|
+
msg = "benchmark requires at least one Python source file"
|
|
58
|
+
raise ValueError(msg)
|
|
59
|
+
corpus_root = Path(commonpath([path.absolute().parent for path in expanded]))
|
|
60
|
+
source_digest = hashlib.sha256()
|
|
61
|
+
source_bytes = 0
|
|
62
|
+
for path in expanded:
|
|
63
|
+
source = path.read_bytes()
|
|
64
|
+
source_bytes += len(source)
|
|
65
|
+
source_digest.update(str(path.absolute().relative_to(corpus_root)).encode())
|
|
66
|
+
source_digest.update(b"\0")
|
|
67
|
+
source_digest.update(source)
|
|
68
|
+
source_digest.update(b"\0")
|
|
69
|
+
samples: list[BenchmarkSample] = []
|
|
70
|
+
for _ in range(repeats):
|
|
71
|
+
start = perf_counter()
|
|
72
|
+
diagnostics = analyze(selected, expanded)
|
|
73
|
+
elapsed = perf_counter() - start
|
|
74
|
+
digest = hashlib.sha256()
|
|
75
|
+
for diagnostic in diagnostics:
|
|
76
|
+
digest.update(
|
|
77
|
+
json.dumps(
|
|
78
|
+
[
|
|
79
|
+
str(diagnostic.path.absolute().relative_to(corpus_root)),
|
|
80
|
+
diagnostic.line,
|
|
81
|
+
diagnostic.col,
|
|
82
|
+
diagnostic.code,
|
|
83
|
+
diagnostic.message,
|
|
84
|
+
diagnostic.severity.value,
|
|
85
|
+
diagnostic.column_encoding.value,
|
|
86
|
+
],
|
|
87
|
+
ensure_ascii=False,
|
|
88
|
+
).encode()
|
|
89
|
+
)
|
|
90
|
+
digest.update(b"\n")
|
|
91
|
+
samples.append(BenchmarkSample(elapsed, len(diagnostics), digest.hexdigest(), peak_memory_bytes()))
|
|
92
|
+
if len({sample.diagnostic_digest for sample in samples}) != 1:
|
|
93
|
+
msg = "diagnostics changed between benchmark repetitions"
|
|
94
|
+
raise RuntimeError(msg)
|
|
95
|
+
return BenchmarkReport(
|
|
96
|
+
len(expanded),
|
|
97
|
+
source_bytes,
|
|
98
|
+
source_digest.hexdigest(),
|
|
99
|
+
tuple(selected),
|
|
100
|
+
tuple(samples),
|
|
101
|
+
median(sample.seconds for sample in samples),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _run(
|
|
106
|
+
paths: Annotated[list[Path], typer.Argument(help="Python files or directories to analyze.")],
|
|
107
|
+
repeats: Annotated[int, typer.Option(min=1)] = 5,
|
|
108
|
+
rule: Annotated[list[str] | None, typer.Option(help="Repeat to select rules; defaults to all rules.")] = None,
|
|
109
|
+
) -> None:
|
|
110
|
+
try:
|
|
111
|
+
report = measure(paths, rule, repeats=repeats)
|
|
112
|
+
except ValueError as exc:
|
|
113
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
114
|
+
typer.echo(json.dumps(asdict(report), indent=2))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
typer.run(_run)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
import ast
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import re
|
|
9
|
+
from typing import TYPE_CHECKING, ClassVar, Final
|
|
10
|
+
|
|
11
|
+
from sarj_rule_contracts import (
|
|
12
|
+
AutofixPolicy as AutofixPolicy,
|
|
13
|
+
ExampleFile as ExampleFile,
|
|
14
|
+
ExampleOutcome as ExampleOutcome,
|
|
15
|
+
NativeRuleSpec as NativeRuleSpec,
|
|
16
|
+
RuleCategory as RuleCategory,
|
|
17
|
+
RuleDocumentation as RuleDocumentation,
|
|
18
|
+
RuleExample as RuleExample,
|
|
19
|
+
Severity as Severity,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from sarj_python_lint._python_target import PythonTargetFacts
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from collections.abc import Sequence
|
|
27
|
+
|
|
28
|
+
from sarj_python_lint._analysis_session import AnalysisSession
|
|
29
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
30
|
+
from sarj_python_lint.rules._project_index import ProjectIndexSet
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Each rule points directly to its executable examples.
|
|
34
|
+
REPO_BLOB: Final = "https://github.com/sarj-ai/code-standards/blob/main"
|
|
35
|
+
TESTS_DIR: Final = "packages/python/tests/rules"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Keep SARJ suppressions separate because Ruff removes unknown `noqa` codes.
|
|
39
|
+
_SARJ_NOQA_RE = re.compile(
|
|
40
|
+
r"#\s*sarj-noqa(?::\s*([A-Za-z0-9_, ]+))?",
|
|
41
|
+
re.IGNORECASE,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def is_suppressed(source_lines: Sequence[str], line: int, code: str) -> bool:
|
|
46
|
+
if line < 1 or line > len(source_lines):
|
|
47
|
+
return False
|
|
48
|
+
text = source_lines[line - 1]
|
|
49
|
+
m = _SARJ_NOQA_RE.search(text)
|
|
50
|
+
if not m:
|
|
51
|
+
return False
|
|
52
|
+
codes_str = m.group(1)
|
|
53
|
+
if not codes_str:
|
|
54
|
+
# A bare sarj-noqa intentionally suppresses every SARJ code on its line.
|
|
55
|
+
return True
|
|
56
|
+
codes = {val.upper() for c in codes_str.split(",") if (val := c.strip())}
|
|
57
|
+
return code.upper() in codes
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ColumnEncoding(StrEnum):
|
|
61
|
+
UTF8_BYTES = "utf8-bytes"
|
|
62
|
+
CODEPOINTS = "codepoints"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True, slots=True)
|
|
66
|
+
class Diagnostic:
|
|
67
|
+
path: Path
|
|
68
|
+
line: int
|
|
69
|
+
col: int
|
|
70
|
+
code: str
|
|
71
|
+
message: str
|
|
72
|
+
severity: Severity = Severity.ERROR
|
|
73
|
+
column_encoding: ColumnEncoding = ColumnEncoding.UTF8_BYTES
|
|
74
|
+
|
|
75
|
+
def format(self) -> str:
|
|
76
|
+
label = "warning: " if self.severity is Severity.WARNING else ""
|
|
77
|
+
return f"{self.path}:{self.line}:{self.col}: {self.code} {label}{self.message}"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Rule(ABC):
|
|
81
|
+
id: str
|
|
82
|
+
code: str
|
|
83
|
+
description: str
|
|
84
|
+
documentation: ClassVar[RuleDocumentation | None] = None
|
|
85
|
+
_analysis_session: AnalysisSession | None = None
|
|
86
|
+
|
|
87
|
+
def prepare_session(self, session: AnalysisSession) -> None:
|
|
88
|
+
self._analysis_session = session
|
|
89
|
+
|
|
90
|
+
def has_declared_python_support_before(self, path: Path, minimum: tuple[int, int]) -> bool:
|
|
91
|
+
facts = self._analysis_session.python_target if self._analysis_session is not None else PythonTargetFacts()
|
|
92
|
+
return facts.has_declared_support_before(path, minimum)
|
|
93
|
+
|
|
94
|
+
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
95
|
+
from sarj_python_lint._file_context import PythonFileContext # ruff: ignore[import-outside-top-level] — avoids registry import cycle
|
|
96
|
+
|
|
97
|
+
context = PythonFileContext(path, source, self._analysis_session)
|
|
98
|
+
if isinstance(self, ProjectRule):
|
|
99
|
+
context.session.project = self.project_indexes
|
|
100
|
+
return self.check_context(context)
|
|
101
|
+
|
|
102
|
+
@abstractmethod
|
|
103
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
104
|
+
raise NotImplementedError
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def examples_path(cls) -> str:
|
|
108
|
+
return f"{TESTS_DIR}/test_{cls.__module__.rpartition('.')[2]}.py"
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def examples_url(cls) -> str:
|
|
112
|
+
return f"{REPO_BLOB}/{cls.examples_path()}"
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def native_spec(cls) -> NativeRuleSpec | None:
|
|
116
|
+
authored = cls.documentation
|
|
117
|
+
if authored is None:
|
|
118
|
+
return None
|
|
119
|
+
if cls.id in authored.aliases:
|
|
120
|
+
msg = f"{cls.id}: a live rule ID cannot also be a historical alias"
|
|
121
|
+
raise ValueError(msg)
|
|
122
|
+
if authored.summary != cls.description:
|
|
123
|
+
msg = f"{cls.id}: description must be the authored documentation summary"
|
|
124
|
+
raise ValueError(msg)
|
|
125
|
+
return NativeRuleSpec(
|
|
126
|
+
engine="python",
|
|
127
|
+
rule_id=cls.id,
|
|
128
|
+
code=cls.code,
|
|
129
|
+
summary=authored.summary,
|
|
130
|
+
rationale=authored.rationale,
|
|
131
|
+
remediation=authored.remediation,
|
|
132
|
+
category=authored.category,
|
|
133
|
+
default_level=authored.default_level,
|
|
134
|
+
autofix=authored.autofix,
|
|
135
|
+
aliases=authored.aliases,
|
|
136
|
+
limitations=authored.limitations,
|
|
137
|
+
examples=authored.examples,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def public_examples(cls) -> tuple[RuleExample, ...]:
|
|
142
|
+
spec = cls.native_spec()
|
|
143
|
+
return () if spec is None else spec.public_examples
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class ProjectRule(Rule, ABC):
|
|
147
|
+
project_indexes: ProjectIndexSet | None = None
|
|
148
|
+
|
|
149
|
+
def prepare(self, indexes: ProjectIndexSet) -> None:
|
|
150
|
+
self.project_indexes = indexes
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def parse_or_none(path: Path, source: str) -> ast.Module | None:
|
|
154
|
+
try:
|
|
155
|
+
tree = ast.parse(source, filename=str(path))
|
|
156
|
+
except SyntaxError:
|
|
157
|
+
tree = None
|
|
158
|
+
return tree
|
|
@@ -28,7 +28,10 @@ def children(node: ast.AST) -> list[ast.AST]:
|
|
|
28
28
|
return out
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def walk(node: ast.AST) -> Iterator[ast.AST]:
|
|
31
|
+
def walk(node: ast.AST, *, index: NodeIndex | None = None) -> Iterator[ast.AST]:
|
|
32
|
+
if index is not None and index.tree is node:
|
|
33
|
+
yield from index.query((ast.AST,))
|
|
34
|
+
return
|
|
32
35
|
queue: list[ast.AST] = [node]
|
|
33
36
|
i = 0
|
|
34
37
|
while i < len(queue):
|
|
@@ -39,10 +42,11 @@ def walk(node: ast.AST) -> Iterator[ast.AST]:
|
|
|
39
42
|
|
|
40
43
|
|
|
41
44
|
@final
|
|
42
|
-
class
|
|
43
|
-
__slots__ = ("_buckets", "_flat", "_queries")
|
|
45
|
+
class NodeIndex:
|
|
46
|
+
__slots__ = ("_buckets", "_flat", "_queries", "tree")
|
|
44
47
|
|
|
45
48
|
def __init__(self, tree: ast.AST) -> None:
|
|
49
|
+
self.tree = tree
|
|
46
50
|
buckets: dict[type[ast.AST], list[ast.AST]] = {}
|
|
47
51
|
flat: list[ast.AST] = [tree]
|
|
48
52
|
i = 0
|
|
@@ -80,13 +84,6 @@ class _NodeIndex:
|
|
|
80
84
|
return result
|
|
81
85
|
|
|
82
86
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def nodes[NodeT: ast.AST](tree: ast.AST, *types: type[NodeT]) -> list[NodeT]:
|
|
88
|
-
global _last_index # ruff: ignore[global-statement] — single-slot memo, mirroring `parse_or_none`
|
|
89
|
-
if _last_index is None or _last_index[0] is not tree:
|
|
90
|
-
_last_index = (tree, _NodeIndex(tree))
|
|
91
|
-
# The `isinstance` pass is what narrows `list[ast.AST]` to `list[NodeT]`
|
|
92
|
-
return [node for node in _last_index[1].query(types) if isinstance(node, types)]
|
|
87
|
+
def nodes[NodeT: ast.AST](tree: ast.AST, *types: type[NodeT], index: NodeIndex | None = None) -> list[NodeT]:
|
|
88
|
+
selected = index if index is not None and index.tree is tree else NodeIndex(tree)
|
|
89
|
+
return [node for node in selected.query(types) if isinstance(node, types)]
|
|
@@ -5,6 +5,7 @@ from collections import Counter
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from typing import TYPE_CHECKING, Literal, NamedTuple
|
|
7
7
|
|
|
8
|
+
from sarj_python_lint.rules._ast_index import walk as walk_ast
|
|
8
9
|
from sarj_python_lint.rules._imports import ImportIndex
|
|
9
10
|
|
|
10
11
|
|
|
@@ -153,7 +154,7 @@ class FastapiIndex:
|
|
|
153
154
|
visit(tree, 0, None)
|
|
154
155
|
|
|
155
156
|
def _read_imports(self, tree: ast.Module) -> None:
|
|
156
|
-
for node in
|
|
157
|
+
for node in walk_ast(tree):
|
|
157
158
|
if self._node_scopes[id(node)] != 0:
|
|
158
159
|
continue
|
|
159
160
|
if isinstance(node, ast.Import):
|
|
@@ -197,7 +198,7 @@ class FastapiIndex:
|
|
|
197
198
|
|
|
198
199
|
def _read_aliases(self, tree: ast.Module) -> None:
|
|
199
200
|
assignments: list[tuple[int, str, ast.expr]] = []
|
|
200
|
-
for node in
|
|
201
|
+
for node in walk_ast(tree):
|
|
201
202
|
self._read_bound_names(node)
|
|
202
203
|
assignment = self._alias_assignment(node)
|
|
203
204
|
if assignment is not None:
|
|
@@ -638,7 +639,7 @@ def _statement_binds(statement: ast.stmt, name: str) -> bool:
|
|
|
638
639
|
|
|
639
640
|
def _stored_names(node: ast.expr) -> frozenset[str]:
|
|
640
641
|
return frozenset(
|
|
641
|
-
child.id for child in
|
|
642
|
+
child.id for child in walk_ast(node) if isinstance(child, ast.Name) and isinstance(child.ctx, ast.Store)
|
|
642
643
|
)
|
|
643
644
|
|
|
644
645
|
|
{sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_fixed_record.py
RENAMED
|
@@ -5,6 +5,8 @@ from dataclasses import dataclass, field
|
|
|
5
5
|
from enum import Enum, auto
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
|
+
from sarj_python_lint.rules._ast_index import walk as walk_ast
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
if TYPE_CHECKING:
|
|
10
12
|
from sarj_python_lint.rules._imports import ImportIndex
|
|
@@ -164,7 +166,7 @@ class _Analyzer:
|
|
|
164
166
|
|
|
165
167
|
def _loop_statement(self, statement: ast.stmt, state: _State) -> _Flow:
|
|
166
168
|
self.invalidate_loop(statement, state)
|
|
167
|
-
has_return = any(isinstance(child, ast.Return) for child in
|
|
169
|
+
has_return = any(isinstance(child, ast.Return) for child in walk_ast(statement))
|
|
168
170
|
returned = [_ReturnKind.UNKNOWN] if has_return else []
|
|
169
171
|
return _Flow(active=[state], returned=returned)
|
|
170
172
|
|
|
@@ -173,7 +175,7 @@ class _Analyzer:
|
|
|
173
175
|
state.detach(name)
|
|
174
176
|
|
|
175
177
|
def _inspect_fallback(self, statement: ast.stmt, state: _State) -> None:
|
|
176
|
-
for child in
|
|
178
|
+
for child in walk_ast(statement):
|
|
177
179
|
match child:
|
|
178
180
|
case ast.Call():
|
|
179
181
|
self.call(child, state)
|
|
@@ -424,7 +426,7 @@ class _Analyzer:
|
|
|
424
426
|
def escape_expression(self, value: ast.expr, state: _State) -> None:
|
|
425
427
|
if isinstance(value, (ast.Lambda, ast.Dict, ast.List, ast.Set, ast.Tuple)):
|
|
426
428
|
self._mark_loaded_records_open(value, state)
|
|
427
|
-
for child in
|
|
429
|
+
for child in walk_ast(value):
|
|
428
430
|
match child:
|
|
429
431
|
case ast.Call():
|
|
430
432
|
self.call(child, state)
|
|
@@ -455,7 +457,7 @@ class _Analyzer:
|
|
|
455
457
|
state.detach(name)
|
|
456
458
|
|
|
457
459
|
def invalidate_loop(self, node: ast.stmt, state: _State) -> None:
|
|
458
|
-
for child in
|
|
460
|
+
for child in walk_ast(node):
|
|
459
461
|
match child:
|
|
460
462
|
case ast.Name(id=name, ctx=ast.Store()):
|
|
461
463
|
record = state.record(name)
|
|
@@ -504,13 +506,13 @@ def _root_name(node: ast.expr) -> str | None:
|
|
|
504
506
|
|
|
505
507
|
|
|
506
508
|
def _loaded_names(node: ast.AST) -> set[str]:
|
|
507
|
-
return {child.id for child in
|
|
509
|
+
return {child.id for child in walk_ast(node) if isinstance(child, ast.Name) and isinstance(child.ctx, ast.Load)}
|
|
508
510
|
|
|
509
511
|
|
|
510
512
|
def _stored_names(node: ast.AST) -> set[str]:
|
|
511
513
|
return {
|
|
512
514
|
child.id
|
|
513
|
-
for child in
|
|
515
|
+
for child in walk_ast(node)
|
|
514
516
|
if isinstance(child, ast.Name) and isinstance(child.ctx, (ast.Store, ast.Del))
|
|
515
517
|
}
|
|
516
518
|
|
|
@@ -521,7 +523,7 @@ def _is_unguarded_irrefutable(case: ast.match_case) -> bool:
|
|
|
521
523
|
|
|
522
524
|
def _pattern_names(pattern: ast.pattern) -> set[str]:
|
|
523
525
|
names: set[str] = set()
|
|
524
|
-
for node in
|
|
526
|
+
for node in walk_ast(pattern):
|
|
525
527
|
match node:
|
|
526
528
|
case ast.MatchAs(name=name) | ast.MatchStar(name=name) if name is not None:
|
|
527
529
|
names.add(name)
|
|
@@ -535,7 +537,7 @@ def _pattern_names(pattern: ast.pattern) -> set[str]:
|
|
|
535
537
|
def _function_bound_names(node: ast.FunctionDef | ast.AsyncFunctionDef) -> set[str]:
|
|
536
538
|
names = _argument_names(node.args)
|
|
537
539
|
for statement in node.body:
|
|
538
|
-
for child in
|
|
540
|
+
for child in walk_ast(statement):
|
|
539
541
|
names.update(_names_bound_by_node(child))
|
|
540
542
|
return names
|
|
541
543
|
|
|
@@ -572,7 +574,7 @@ def _function_external_names(node: ast.FunctionDef | ast.AsyncFunctionDef) -> se
|
|
|
572
574
|
return {
|
|
573
575
|
name
|
|
574
576
|
for statement in node.body
|
|
575
|
-
for child in
|
|
577
|
+
for child in walk_ast(statement)
|
|
576
578
|
if isinstance(child, (ast.Global, ast.Nonlocal))
|
|
577
579
|
for name in child.names
|
|
578
580
|
}
|
|
@@ -6,6 +6,8 @@ from dataclasses import dataclass
|
|
|
6
6
|
from types import MappingProxyType
|
|
7
7
|
from typing import Self
|
|
8
8
|
|
|
9
|
+
from sarj_python_lint.rules._ast_index import walk as walk_ast
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
@dataclass(frozen=True, slots=True)
|
|
11
13
|
class _ImportTarget:
|
|
@@ -77,7 +79,7 @@ class ImportIndex:
|
|
|
77
79
|
|
|
78
80
|
def _non_import_bindings(tree: ast.Module) -> set[str]:
|
|
79
81
|
names: set[str] = set()
|
|
80
|
-
for node in
|
|
82
|
+
for node in walk_ast(tree):
|
|
81
83
|
match node:
|
|
82
84
|
case ast.Name(id=name, ctx=(ast.Store() | ast.Del())) | ast.arg(arg=name):
|
|
83
85
|
names.add(name)
|
{sarj_python_lint-0.88.0 → sarj_python_lint-1.1.0}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
@@ -10,6 +10,7 @@ import re
|
|
|
10
10
|
from types import MappingProxyType
|
|
11
11
|
from typing import TYPE_CHECKING, final
|
|
12
12
|
|
|
13
|
+
from sarj_python_lint.rules._ast_index import walk as walk_ast
|
|
13
14
|
from sarj_python_lint.rules._first_party import FirstPartyFacts, project_root
|
|
14
15
|
|
|
15
16
|
|
|
@@ -139,7 +140,7 @@ class ProjectIndexSet:
|
|
|
139
140
|
return self._classes.get(symbol) if symbol is not None else None
|
|
140
141
|
|
|
141
142
|
def annotation_contains_enum(self, unit: SourceUnit, annotation: ast.expr) -> bool:
|
|
142
|
-
for member in
|
|
143
|
+
for member in walk_ast(annotation):
|
|
143
144
|
if not isinstance(member, (ast.Name, ast.Attribute)):
|
|
144
145
|
continue
|
|
145
146
|
symbol = self.resolve(unit, member)
|
|
@@ -198,7 +199,7 @@ class ProjectIndexSet:
|
|
|
198
199
|
and name in candidate.source
|
|
199
200
|
and any(
|
|
200
201
|
_is_mock_spec_for(candidate, call, target)
|
|
201
|
-
for call in
|
|
202
|
+
for call in walk_ast(candidate.tree)
|
|
202
203
|
if isinstance(call, ast.Call)
|
|
203
204
|
)
|
|
204
205
|
)
|
|
@@ -614,7 +615,7 @@ def _annotation_contains_symbol(unit: SourceUnit, annotation: ast.expr | None, t
|
|
|
614
615
|
annotation = ast.parse(annotation.value, mode="eval").body
|
|
615
616
|
return any(
|
|
616
617
|
_resolve(unit, candidate) == target
|
|
617
|
-
for candidate in
|
|
618
|
+
for candidate in walk_ast(annotation)
|
|
618
619
|
if isinstance(candidate, (ast.Name, ast.Attribute))
|
|
619
620
|
)
|
|
620
621
|
|