sarj-python-lint 0.88.0__tar.gz → 1.1.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.
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/.gitignore +1 -1
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/PKG-INFO +2 -1
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/pyproject.toml +5 -2
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/__main__.py +25 -17
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/_analysis_session.py +6 -1
- sarj_python_lint-1.1.1/src/sarj_python_lint/_file_context.py +73 -0
- sarj_python_lint-1.1.1/src/sarj_python_lint/benchmark.py +118 -0
- sarj_python_lint-1.1.1/src/sarj_python_lint/rule_base.py +158 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_ast_index.py +22 -12
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_fastapi.py +4 -3
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_fixed_record.py +11 -9
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_imports.py +3 -1
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_project_index.py +4 -3
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_prose_budget.py +3 -3
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +65 -44
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +8 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +8 -7
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +8 -7
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/excessive_commentary.py +10 -7
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/fakes_in_shared_location.py +7 -5
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +26 -18
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +12 -12
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +8 -9
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +12 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/mock_without_spec.py +29 -13
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/named_record_at_boundaries.py +17 -19
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +9 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/src/sarj_python_lint/rules/no_any_mapping_types.py +23 -12
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_comment_cruft.py +6 -3
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +5 -6
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +6 -5
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +8 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_delete_statement.py +8 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_dunder_all.py +6 -6
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +7 -7
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +7 -7
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_fastapi_on_event.py +7 -5
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +27 -17
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +9 -7
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +9 -7
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +8 -12
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +5 -4
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +13 -15
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_offset_pagination.py +10 -10
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +15 -11
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +6 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +10 -7
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +9 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +8 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +7 -5
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_repeated_test_body.py +8 -6
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_restated_comment.py +14 -14
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_secret_in_log.py +6 -5
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_select_star.py +17 -14
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +11 -9
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +7 -6
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_tautological_expect.py +13 -12
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +14 -11
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +23 -18
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +45 -25
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_unused_value_marker.py +17 -14
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +5 -5
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +16 -13
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +21 -13
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/over_mocked_test.py +7 -5
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/phase_label_comment.py +6 -5
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_class_row.py +16 -9
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +23 -18
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +23 -20
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +18 -16
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +6 -6
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +21 -14
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_library_fake.py +7 -5
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +17 -13
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +30 -17
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +10 -9
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +17 -16
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +25 -22
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +18 -11
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +15 -11
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +17 -13
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +8 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +9 -7
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +21 -15
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +22 -19
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +14 -10
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_str_enum.py +46 -27
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +30 -20
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +13 -12
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +16 -12
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +19 -13
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +14 -10
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +10 -7
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +9 -9
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +12 -12
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/redundant_docstring.py +17 -13
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +14 -9
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +9 -7
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/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.1}/src/sarj_python_lint/rules/require_port_for_service.py +31 -20
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +34 -20
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +16 -14
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +11 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/restated_test_docstring.py +20 -15
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/stepdown.py +7 -5
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +19 -18
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +7 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/trailing_value_narration.py +12 -10
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/typed_error_reasons.py +11 -8
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +7 -5
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/unused_mock_setup.py +11 -10
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.1}/LICENSE +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/README.md +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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.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/
|
|
@@ -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.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" }]
|
|
@@ -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 }
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import ast
|
|
4
4
|
from collections import Counter
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
+
from fnmatch import fnmatch
|
|
6
7
|
import json
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
import sys
|
|
@@ -13,6 +14,7 @@ import typer
|
|
|
13
14
|
|
|
14
15
|
from sarj_python_lint import __version__
|
|
15
16
|
from sarj_python_lint._analysis_session import AnalysisSession
|
|
17
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
16
18
|
from sarj_python_lint._filesystem import atomic_write_text
|
|
17
19
|
from sarj_python_lint.json_boundary import is_object_mapping, parse_json
|
|
18
20
|
from sarj_python_lint.rule_base import Diagnostic, ProjectRule, Rule, Severity, is_suppressed
|
|
@@ -47,7 +49,7 @@ SKIP_DIR_NAMES = frozenset(
|
|
|
47
49
|
_MAX_FILE_BYTES = 500_000
|
|
48
50
|
|
|
49
51
|
|
|
50
|
-
def
|
|
52
|
+
def expand_paths(paths: list[Path]) -> list[Path]:
|
|
51
53
|
out: list[Path] = []
|
|
52
54
|
for p in paths:
|
|
53
55
|
if not p.exists():
|
|
@@ -65,19 +67,21 @@ def _expand_paths(paths: list[Path]) -> list[Path]:
|
|
|
65
67
|
|
|
66
68
|
|
|
67
69
|
def _python_files(p: Path) -> list[Path]:
|
|
70
|
+
if not SKIP_DIR_NAMES.isdisjoint(p.parts):
|
|
71
|
+
return []
|
|
68
72
|
out: list[Path] = []
|
|
69
|
-
for
|
|
70
|
-
if not
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
continue
|
|
74
|
-
try:
|
|
75
|
-
if child.stat().st_size > _MAX_FILE_BYTES:
|
|
73
|
+
for directory, directories, filenames in p.walk():
|
|
74
|
+
directories[:] = [name for name in directories if name not in SKIP_DIR_NAMES]
|
|
75
|
+
for name in filenames:
|
|
76
|
+
if not fnmatch(name, "*.py"):
|
|
76
77
|
continue
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
child = directory / name
|
|
79
|
+
try:
|
|
80
|
+
if child.is_file() and child.stat().st_size <= _MAX_FILE_BYTES:
|
|
81
|
+
out.append(child)
|
|
82
|
+
except OSError:
|
|
83
|
+
continue
|
|
84
|
+
return sorted(out)
|
|
81
85
|
|
|
82
86
|
|
|
83
87
|
def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
|
|
@@ -91,7 +95,7 @@ def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
|
|
|
91
95
|
for rule in rules:
|
|
92
96
|
rule.prepare_session(session)
|
|
93
97
|
clear_path_caches()
|
|
94
|
-
expanded =
|
|
98
|
+
expanded = expand_paths(paths)
|
|
95
99
|
loaded: dict[Path, str] = {}
|
|
96
100
|
for path in expanded:
|
|
97
101
|
try:
|
|
@@ -101,17 +105,21 @@ def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
|
|
|
101
105
|
project_rules = [rule for rule in rules if isinstance(rule, ProjectRule)]
|
|
102
106
|
if project_rules:
|
|
103
107
|
indexes = ProjectIndexSet.build(expanded, loaded, facts=session.first_party)
|
|
108
|
+
session.project = indexes
|
|
104
109
|
for rule in project_rules:
|
|
105
110
|
rule.prepare(indexes)
|
|
106
111
|
diags: list[Diagnostic] = []
|
|
107
112
|
for p, source in loaded.items():
|
|
108
|
-
diags.extend(
|
|
113
|
+
diags.extend(check_source(rules, p, source, session=session))
|
|
109
114
|
return diags
|
|
110
115
|
|
|
111
116
|
|
|
112
|
-
def
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
def check_source(
|
|
118
|
+
rules: list[Rule], p: Path, source: str, *, session: AnalysisSession | None = None
|
|
119
|
+
) -> list[Diagnostic]:
|
|
120
|
+
context = PythonFileContext(p, source, session)
|
|
121
|
+
source_lines = context.source_lines
|
|
122
|
+
raw = [diagnostic for rule in rules for diagnostic in rule.check_context(context)]
|
|
115
123
|
return deduplicate_diagnostics(
|
|
116
124
|
[
|
|
117
125
|
diagnostic
|
{sarj_python_lint-0.88.0 → sarj_python_lint-1.1.1}/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,73 @@
|
|
|
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 collections.abc import Mapping
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@final
|
|
20
|
+
class PythonFileContext:
|
|
21
|
+
def __init__(self, path: Path, source: str, session: AnalysisSession | None = None) -> None:
|
|
22
|
+
self.path = path
|
|
23
|
+
self.source = source
|
|
24
|
+
self.session = session if session is not None else AnalysisSession()
|
|
25
|
+
|
|
26
|
+
@cached_property
|
|
27
|
+
def tree(self) -> ast.Module | None:
|
|
28
|
+
if self.session.project is not None:
|
|
29
|
+
unit = self.session.project.unit(self.path)
|
|
30
|
+
if unit is not None and unit.source == self.source:
|
|
31
|
+
return unit.tree
|
|
32
|
+
try:
|
|
33
|
+
return ast.parse(self.source, filename=str(self.path))
|
|
34
|
+
except SyntaxError:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
@cached_property
|
|
38
|
+
def source_lines(self) -> list[str]:
|
|
39
|
+
return self.source.splitlines()
|
|
40
|
+
|
|
41
|
+
@cached_property
|
|
42
|
+
def generated(self) -> bool:
|
|
43
|
+
return is_generated(self.path, self.source)
|
|
44
|
+
|
|
45
|
+
@cached_property
|
|
46
|
+
def node_index(self) -> NodeIndex:
|
|
47
|
+
return NodeIndex(self._valid_tree())
|
|
48
|
+
|
|
49
|
+
def nodes[NodeT: ast.AST](self, *types: type[NodeT]) -> list[NodeT]:
|
|
50
|
+
return [node for node in self.node_index.query(types) if isinstance(node, types)]
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def parents(self) -> Mapping[ast.AST, ast.AST]:
|
|
54
|
+
return self.node_index.parents
|
|
55
|
+
|
|
56
|
+
@cached_property
|
|
57
|
+
def imports(self) -> ImportIndex:
|
|
58
|
+
return ImportIndex.from_tree(self._valid_tree())
|
|
59
|
+
|
|
60
|
+
@cached_property
|
|
61
|
+
def module_imports(self) -> ImportIndex:
|
|
62
|
+
return ImportIndex.from_tree(self._valid_tree(), module_scope_only=True)
|
|
63
|
+
|
|
64
|
+
@cached_property
|
|
65
|
+
def fastapi(self) -> FastapiIndex:
|
|
66
|
+
return FastapiIndex(self._valid_tree(), path=self.path)
|
|
67
|
+
|
|
68
|
+
def _valid_tree(self) -> ast.Module:
|
|
69
|
+
tree = self.tree
|
|
70
|
+
if tree is None:
|
|
71
|
+
msg = "syntax-dependent facts require a successfully parsed source file"
|
|
72
|
+
raise ValueError(msg)
|
|
73
|
+
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
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import ast
|
|
6
|
+
from types import MappingProxyType
|
|
6
7
|
from typing import TYPE_CHECKING, TypeIs, final
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
if TYPE_CHECKING:
|
|
10
|
-
from collections.abc import Iterator
|
|
11
|
+
from collections.abc import Iterator, Mapping
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
_AST = ast.AST
|
|
@@ -28,7 +29,10 @@ def children(node: ast.AST) -> list[ast.AST]:
|
|
|
28
29
|
return out
|
|
29
30
|
|
|
30
31
|
|
|
31
|
-
def walk(node: ast.AST) -> Iterator[ast.AST]:
|
|
32
|
+
def walk(node: ast.AST, *, index: NodeIndex | None = None) -> Iterator[ast.AST]:
|
|
33
|
+
if index is not None and index.tree is node:
|
|
34
|
+
yield from index.query((ast.AST,))
|
|
35
|
+
return
|
|
32
36
|
queue: list[ast.AST] = [node]
|
|
33
37
|
i = 0
|
|
34
38
|
while i < len(queue):
|
|
@@ -39,10 +43,11 @@ def walk(node: ast.AST) -> Iterator[ast.AST]:
|
|
|
39
43
|
|
|
40
44
|
|
|
41
45
|
@final
|
|
42
|
-
class
|
|
43
|
-
__slots__ = ("_buckets", "_flat", "_queries")
|
|
46
|
+
class NodeIndex:
|
|
47
|
+
__slots__ = ("_buckets", "_flat", "_parents", "_queries", "tree")
|
|
44
48
|
|
|
45
49
|
def __init__(self, tree: ast.AST) -> None:
|
|
50
|
+
self.tree = tree
|
|
46
51
|
buckets: dict[type[ast.AST], list[ast.AST]] = {}
|
|
47
52
|
flat: list[ast.AST] = [tree]
|
|
48
53
|
i = 0
|
|
@@ -58,8 +63,15 @@ class _NodeIndex:
|
|
|
58
63
|
flat += children(current)
|
|
59
64
|
self._buckets: dict[type[ast.AST], list[ast.AST]] = buckets
|
|
60
65
|
self._flat: list[ast.AST] = flat
|
|
66
|
+
self._parents: Mapping[ast.AST, ast.AST] | None = None
|
|
61
67
|
self._queries: dict[tuple[type[ast.AST], ...], list[ast.AST]] = {}
|
|
62
68
|
|
|
69
|
+
@property
|
|
70
|
+
def parents(self) -> Mapping[ast.AST, ast.AST]:
|
|
71
|
+
if self._parents is None:
|
|
72
|
+
self._parents = MappingProxyType({child: parent for parent in self._flat for child in children(parent)})
|
|
73
|
+
return self._parents
|
|
74
|
+
|
|
63
75
|
def query(self, types: tuple[type[ast.AST], ...]) -> list[ast.AST]:
|
|
64
76
|
hit = self._queries.get(types)
|
|
65
77
|
if hit is not None:
|
|
@@ -80,13 +92,11 @@ class _NodeIndex:
|
|
|
80
92
|
return result
|
|
81
93
|
|
|
82
94
|
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
def nodes[NodeT: ast.AST](tree: ast.AST, *types: type[NodeT], index: NodeIndex | None = None) -> list[NodeT]:
|
|
96
|
+
selected = index if index is not None and index.tree is tree else NodeIndex(tree)
|
|
97
|
+
return [node for node in selected.query(types) if isinstance(node, types)]
|
|
85
98
|
|
|
86
99
|
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
|
|
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)]
|
|
100
|
+
def parent_map(tree: ast.AST, *, index: NodeIndex | None = None) -> Mapping[ast.AST, ast.AST]:
|
|
101
|
+
selected = index if index is not None and index.tree is tree else NodeIndex(tree)
|
|
102
|
+
return selected.parents
|
|
@@ -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
|
|