sarj-python-lint 0.80.10__tar.gz → 0.80.12__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.80.10 → sarj_python_lint-0.80.12}/PKG-INFO +1 -1
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/pyproject.toml +1 -1
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/_registry.py +0 -2
- sarj_python_lint-0.80.12/src/sarj_python_lint/rules/trailing_value_narration.py +295 -0
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/trailing_value_narration.py +0 -197
- sarj_python_lint-0.80.10/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -466
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/.gitignore +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/LICENSE +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/README.md +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 0.80.
|
|
3
|
+
Version: 0.80.12
|
|
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/
|
{sarj_python_lint-0.80.10 → sarj_python_lint-0.80.12}/src/sarj_python_lint/rules/_registry.py
RENAMED
|
@@ -146,7 +146,6 @@ from sarj_python_lint.rules.store_insert_requires_on_conflict import (
|
|
|
146
146
|
)
|
|
147
147
|
from sarj_python_lint.rules.timestamp_order_requires_tiebreaker import TimestampOrderRequiresTiebreaker
|
|
148
148
|
from sarj_python_lint.rules.trailing_value_narration import TrailingValueNarration
|
|
149
|
-
from sarj_python_lint.rules.trivially_true_assertion import TriviallyTrueAssertion
|
|
150
149
|
from sarj_python_lint.rules.typed_error_reasons import TypedErrorReasons
|
|
151
150
|
from sarj_python_lint.rules.uncontrolled_randomness_in_test import UncontrolledRandomnessInTest
|
|
152
151
|
from sarj_python_lint.rules.unused_mock_setup import UnusedMockSetup
|
|
@@ -211,7 +210,6 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
211
210
|
OverMockedTest.id: OverMockedTest,
|
|
212
211
|
InvalidPydanticFieldDefault.id: InvalidPydanticFieldDefault,
|
|
213
212
|
NoFrozenAfterValidatorFieldWrite.id: NoFrozenAfterValidatorFieldWrite,
|
|
214
|
-
TriviallyTrueAssertion.id: TriviallyTrueAssertion,
|
|
215
213
|
NoRepeatedTestBody.id: NoRepeatedTestBody,
|
|
216
214
|
UnusedMockSetup.id: UnusedMockSetup,
|
|
217
215
|
PreferFstringOverConcat.id: PreferFstringOverConcat,
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
import re
|
|
6
|
+
import tokenize
|
|
7
|
+
from typing import TYPE_CHECKING, ClassVar, override
|
|
8
|
+
|
|
9
|
+
from sarj_python_lint.rule_base import (
|
|
10
|
+
AutofixPolicy,
|
|
11
|
+
ColumnEncoding,
|
|
12
|
+
Diagnostic,
|
|
13
|
+
ExampleFile,
|
|
14
|
+
ExampleOutcome,
|
|
15
|
+
Rule,
|
|
16
|
+
RuleCategory,
|
|
17
|
+
RuleDocumentation,
|
|
18
|
+
RuleExample,
|
|
19
|
+
Severity,
|
|
20
|
+
parse_or_none,
|
|
21
|
+
)
|
|
22
|
+
from sarj_python_lint.rules._comments import (
|
|
23
|
+
code_tokens,
|
|
24
|
+
has_external_reference,
|
|
25
|
+
nested_comment_lines,
|
|
26
|
+
stem,
|
|
27
|
+
trailing_comments,
|
|
28
|
+
)
|
|
29
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# A number that is not part of an identifier or a dotted attribute path.
|
|
37
|
+
_NUMBER_RE = re.compile(
|
|
38
|
+
r"(?<![\w.])(?:0[xob][0-9a-f_]+|\d[\d_]*(?:\.\d[\d_]*)?(?:e[+-]?\d+)?)(?![\d_.])",
|
|
39
|
+
re.IGNORECASE,
|
|
40
|
+
)
|
|
41
|
+
_WORD_RE = re.compile(r"[A-Za-z]+(?:'[a-z]+)?|\d[\d_]*(?:\.\d[\d_]*)?")
|
|
42
|
+
|
|
43
|
+
# Words that name the unit rather than the quantity — the one thing the code
|
|
44
|
+
# does not say, and the reason the fix is a *name*, not a deletion.
|
|
45
|
+
_UNIT_WORDS = frozenset(
|
|
46
|
+
{
|
|
47
|
+
"bytes",
|
|
48
|
+
"byte",
|
|
49
|
+
"characters",
|
|
50
|
+
"character",
|
|
51
|
+
"chars",
|
|
52
|
+
"day",
|
|
53
|
+
"days",
|
|
54
|
+
"gb",
|
|
55
|
+
"gib",
|
|
56
|
+
"hour",
|
|
57
|
+
"hours",
|
|
58
|
+
"hr",
|
|
59
|
+
"hrs",
|
|
60
|
+
"hz",
|
|
61
|
+
"items",
|
|
62
|
+
"item",
|
|
63
|
+
"k",
|
|
64
|
+
"kb",
|
|
65
|
+
"kib",
|
|
66
|
+
"khz",
|
|
67
|
+
"m",
|
|
68
|
+
"mb",
|
|
69
|
+
"mib",
|
|
70
|
+
"microseconds",
|
|
71
|
+
"milliseconds",
|
|
72
|
+
"min",
|
|
73
|
+
"mins",
|
|
74
|
+
"minute",
|
|
75
|
+
"minutes",
|
|
76
|
+
"ms",
|
|
77
|
+
"pct",
|
|
78
|
+
"percent",
|
|
79
|
+
"px",
|
|
80
|
+
"retries",
|
|
81
|
+
"retry",
|
|
82
|
+
"rows",
|
|
83
|
+
"row",
|
|
84
|
+
"s",
|
|
85
|
+
"sec",
|
|
86
|
+
"second",
|
|
87
|
+
"seconds",
|
|
88
|
+
"secs",
|
|
89
|
+
"times",
|
|
90
|
+
"tokens",
|
|
91
|
+
"token",
|
|
92
|
+
"us",
|
|
93
|
+
"ns",
|
|
94
|
+
"week",
|
|
95
|
+
"weeks",
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
_STOPWORDS = frozenset(
|
|
100
|
+
{
|
|
101
|
+
"a",
|
|
102
|
+
"an",
|
|
103
|
+
"and",
|
|
104
|
+
"are",
|
|
105
|
+
"as",
|
|
106
|
+
"at",
|
|
107
|
+
"be",
|
|
108
|
+
"by",
|
|
109
|
+
"for",
|
|
110
|
+
"in",
|
|
111
|
+
"is",
|
|
112
|
+
"it",
|
|
113
|
+
"of",
|
|
114
|
+
"on",
|
|
115
|
+
"or",
|
|
116
|
+
"that",
|
|
117
|
+
"the",
|
|
118
|
+
"this",
|
|
119
|
+
"we",
|
|
120
|
+
"with",
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
_DIRECTIVE_RE = re.compile(
|
|
125
|
+
r"^\s*(?:todo|fixme|hack\b|xxx|noqa|sarj-noqa|type:|pragma|pyright|mypy|fmt:|isort|ruff|"
|
|
126
|
+
r"pylint|flake8|nosec|nosemgrep)",
|
|
127
|
+
re.IGNORECASE,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _narrates_value(body: str, code: str, code_numbers: frozenset[str]) -> bool:
|
|
132
|
+
if not body or _DIRECTIVE_RE.match(body) or has_external_reference(body):
|
|
133
|
+
return False
|
|
134
|
+
if not code_numbers:
|
|
135
|
+
return False
|
|
136
|
+
words = [match.group(0).lower() for match in _WORD_RE.finditer(body)]
|
|
137
|
+
if not words:
|
|
138
|
+
return False
|
|
139
|
+
comment_number_texts = [match.group(0) for match in _NUMBER_RE.finditer(body)]
|
|
140
|
+
if len(comment_number_texts) != 1:
|
|
141
|
+
return False
|
|
142
|
+
comment_number = _number_key(comment_number_texts[0])
|
|
143
|
+
if comment_number is None or comment_number not in code_numbers:
|
|
144
|
+
return False
|
|
145
|
+
if not any(word in _UNIT_WORDS for word in words):
|
|
146
|
+
return False
|
|
147
|
+
identifiers = code_tokens(code)
|
|
148
|
+
identifier_stems = {stem(token) for token in identifiers}
|
|
149
|
+
for word in words:
|
|
150
|
+
if word in _STOPWORDS or word in _UNIT_WORDS or _number_key(word) == comment_number:
|
|
151
|
+
continue
|
|
152
|
+
if word in identifiers or stem(word) in identifier_stems:
|
|
153
|
+
continue
|
|
154
|
+
return False
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class TrailingValueNarration(Rule):
|
|
159
|
+
id: str = "no-trailing-numeric-unit-assignment-comment"
|
|
160
|
+
code: str = "SARJ051"
|
|
161
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
162
|
+
summary="A trailing comment redundantly labels a numeric assignment with its value and unit.",
|
|
163
|
+
rationale=(
|
|
164
|
+
"A unit encoded only in a comment can drift; a unit-bearing name makes it machine- and "
|
|
165
|
+
"reviewer-visible, while a duration type can enforce it."
|
|
166
|
+
),
|
|
167
|
+
remediation=(
|
|
168
|
+
"Encode or reconcile the unit in the assigned name or typed value, such as timeout_ms or "
|
|
169
|
+
"timedelta(minutes=5), then delete the duplicate comment."
|
|
170
|
+
),
|
|
171
|
+
category=RuleCategory.MAINTAINABILITY,
|
|
172
|
+
autofix=AutofixPolicy.SUGGESTION,
|
|
173
|
+
aliases=("trailing-value-narration",),
|
|
174
|
+
limitations=(
|
|
175
|
+
"Detection targets one-line numeric assignments with one simple name or attribute target and a trailing comment that repeats one number and unit.",
|
|
176
|
+
"Calls, returns, loops, assertions, chained or destructured assignments, multi-number comments, approximate conversions, reasons, references, directives, bracketed values, invalid syntax, and generated files are excluded.",
|
|
177
|
+
),
|
|
178
|
+
examples=(
|
|
179
|
+
RuleExample(
|
|
180
|
+
example_id="repeated-numeric-unit",
|
|
181
|
+
title="Comment repeats the duration",
|
|
182
|
+
outcome=ExampleOutcome.MATCH,
|
|
183
|
+
files=(ExampleFile.python("settings.py", "STALE_TIME = 5 * 60 * 1000 # 5 minutes\n"),),
|
|
184
|
+
focus_path=PurePosixPath("settings.py"),
|
|
185
|
+
expected_count=1,
|
|
186
|
+
public=True,
|
|
187
|
+
),
|
|
188
|
+
RuleExample(
|
|
189
|
+
example_id="unit-encoded-in-value",
|
|
190
|
+
title="Typed value carries the unit",
|
|
191
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
192
|
+
files=(
|
|
193
|
+
ExampleFile.python(
|
|
194
|
+
"settings.py",
|
|
195
|
+
"from datetime import timedelta\n\nSTALE_TIME = timedelta(minutes=5)\n",
|
|
196
|
+
),
|
|
197
|
+
),
|
|
198
|
+
focus_path=PurePosixPath("settings.py"),
|
|
199
|
+
expected_count=0,
|
|
200
|
+
public=True,
|
|
201
|
+
),
|
|
202
|
+
),
|
|
203
|
+
)
|
|
204
|
+
description: str = documentation.summary
|
|
205
|
+
|
|
206
|
+
@override
|
|
207
|
+
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
208
|
+
if is_generated(path, source):
|
|
209
|
+
return []
|
|
210
|
+
try:
|
|
211
|
+
trailing = trailing_comments(source)
|
|
212
|
+
nested = nested_comment_lines(source)
|
|
213
|
+
except tokenize.TokenError, IndentationError, SyntaxError:
|
|
214
|
+
return []
|
|
215
|
+
if not trailing:
|
|
216
|
+
return []
|
|
217
|
+
tree = parse_or_none(path, source)
|
|
218
|
+
if tree is None:
|
|
219
|
+
return []
|
|
220
|
+
lines = source.splitlines()
|
|
221
|
+
assignments = _numeric_assignments(tree)
|
|
222
|
+
diags: list[Diagnostic] = []
|
|
223
|
+
for line, col, body in trailing:
|
|
224
|
+
if line > len(lines) or line in nested:
|
|
225
|
+
continue
|
|
226
|
+
if (numbers := assignments.get(line)) is not None and _narrates_value(body, lines[line - 1][:col], numbers):
|
|
227
|
+
diags.append(
|
|
228
|
+
Diagnostic(
|
|
229
|
+
path=path,
|
|
230
|
+
line=line,
|
|
231
|
+
col=col + 1,
|
|
232
|
+
code=self.code,
|
|
233
|
+
message=(
|
|
234
|
+
f"Trailing numeric-unit label {body.strip()!r} can drift; encode or reconcile the "
|
|
235
|
+
"unit in the assigned name or value type, then remove the comment."
|
|
236
|
+
),
|
|
237
|
+
severity=Severity.WARNING,
|
|
238
|
+
column_encoding=ColumnEncoding.CODEPOINTS,
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
return diags
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _numeric_assignments(tree: ast.AST) -> dict[int, frozenset[str]]:
|
|
245
|
+
statements_per_line: dict[int, int] = {}
|
|
246
|
+
candidates: dict[int, list[frozenset[str]]] = {}
|
|
247
|
+
for statement in ast.walk(tree):
|
|
248
|
+
if not isinstance(statement, ast.stmt):
|
|
249
|
+
continue
|
|
250
|
+
statements_per_line[statement.lineno] = statements_per_line.get(statement.lineno, 0) + 1
|
|
251
|
+
if not isinstance(statement, (ast.Assign, ast.AnnAssign)) or statement.end_lineno != statement.lineno:
|
|
252
|
+
continue
|
|
253
|
+
targets = statement.targets if isinstance(statement, ast.Assign) else [statement.target]
|
|
254
|
+
if len(targets) != 1 or not isinstance(targets[0], (ast.Name, ast.Attribute)):
|
|
255
|
+
continue
|
|
256
|
+
value = statement.value
|
|
257
|
+
if value is None or (numbers := _numeric_expression_numbers(value)) is None or not numbers:
|
|
258
|
+
continue
|
|
259
|
+
candidates.setdefault(statement.lineno, []).append(numbers)
|
|
260
|
+
return {
|
|
261
|
+
line: values[0]
|
|
262
|
+
for line, values in candidates.items()
|
|
263
|
+
if len(values) == 1 and statements_per_line.get(line) == 1
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _numeric_expression_numbers(node: ast.expr) -> frozenset[str] | None:
|
|
268
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)) and not isinstance(node.value, bool):
|
|
269
|
+
key = _number_key(node.value)
|
|
270
|
+
return frozenset() if key is None else frozenset({key})
|
|
271
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.UAdd, ast.USub)):
|
|
272
|
+
return _numeric_expression_numbers(node.operand)
|
|
273
|
+
if isinstance(node, ast.BinOp) and isinstance(
|
|
274
|
+
node.op,
|
|
275
|
+
(ast.Add, ast.Sub, ast.Mult, ast.Div, ast.FloorDiv, ast.Mod, ast.Pow),
|
|
276
|
+
):
|
|
277
|
+
left = _numeric_expression_numbers(node.left)
|
|
278
|
+
right = _numeric_expression_numbers(node.right)
|
|
279
|
+
return None if left is None or right is None else left | right
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _number_key(value: str | float) -> str | None:
|
|
284
|
+
parsed: object = value
|
|
285
|
+
if isinstance(value, str):
|
|
286
|
+
normalized = value.replace("_", "")
|
|
287
|
+
try:
|
|
288
|
+
parsed = float(normalized) if "." in normalized or "e" in normalized.lower() else int(normalized, 0)
|
|
289
|
+
except ValueError:
|
|
290
|
+
return None
|
|
291
|
+
if isinstance(parsed, bool) or not isinstance(parsed, (int, float)):
|
|
292
|
+
return None
|
|
293
|
+
if isinstance(parsed, float) and parsed.is_integer():
|
|
294
|
+
return str(int(parsed))
|
|
295
|
+
return str(parsed)
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from pathlib import PurePosixPath
|
|
4
|
-
import re
|
|
5
|
-
import tokenize
|
|
6
|
-
from typing import TYPE_CHECKING, ClassVar, override
|
|
7
|
-
|
|
8
|
-
from sarj_python_lint.rule_base import (
|
|
9
|
-
AutofixPolicy,
|
|
10
|
-
ColumnEncoding,
|
|
11
|
-
Diagnostic,
|
|
12
|
-
ExampleFile,
|
|
13
|
-
ExampleOutcome,
|
|
14
|
-
Rule,
|
|
15
|
-
RuleCategory,
|
|
16
|
-
RuleDocumentation,
|
|
17
|
-
RuleExample,
|
|
18
|
-
)
|
|
19
|
-
from sarj_python_lint.rules._comments import (
|
|
20
|
-
code_tokens,
|
|
21
|
-
has_external_reference,
|
|
22
|
-
nested_comment_lines,
|
|
23
|
-
stem,
|
|
24
|
-
trailing_comments,
|
|
25
|
-
)
|
|
26
|
-
from sarj_python_lint.rules._paths import is_generated
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if TYPE_CHECKING:
|
|
30
|
-
from pathlib import Path
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
# A number that is not part of an identifier or a dotted attribute path.
|
|
34
|
-
_NUMBER_RE = re.compile(r"(?<![\w.])(\d+(?:\.\d+)?)(?![\w.])")
|
|
35
|
-
_WORD_RE = re.compile(r"[A-Za-z]+(?:'[a-z]+)?|\d+(?:\.\d+)?")
|
|
36
|
-
|
|
37
|
-
# Words that name the unit rather than the quantity — the one thing the code
|
|
38
|
-
# does not say, and the reason the fix is a *name*, not a deletion.
|
|
39
|
-
_UNIT_WORDS = frozenset(
|
|
40
|
-
{
|
|
41
|
-
"bytes",
|
|
42
|
-
"characters",
|
|
43
|
-
"chars",
|
|
44
|
-
"day",
|
|
45
|
-
"days",
|
|
46
|
-
"gb",
|
|
47
|
-
"hour",
|
|
48
|
-
"hours",
|
|
49
|
-
"hr",
|
|
50
|
-
"hrs",
|
|
51
|
-
"hz",
|
|
52
|
-
"items",
|
|
53
|
-
"k",
|
|
54
|
-
"kb",
|
|
55
|
-
"khz",
|
|
56
|
-
"m",
|
|
57
|
-
"mb",
|
|
58
|
-
"milliseconds",
|
|
59
|
-
"min",
|
|
60
|
-
"mins",
|
|
61
|
-
"minute",
|
|
62
|
-
"minutes",
|
|
63
|
-
"ms",
|
|
64
|
-
"pct",
|
|
65
|
-
"percent",
|
|
66
|
-
"px",
|
|
67
|
-
"retries",
|
|
68
|
-
"rows",
|
|
69
|
-
"s",
|
|
70
|
-
"sec",
|
|
71
|
-
"second",
|
|
72
|
-
"seconds",
|
|
73
|
-
"secs",
|
|
74
|
-
"times",
|
|
75
|
-
"tokens",
|
|
76
|
-
}
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
_STOPWORDS = frozenset(
|
|
80
|
-
{
|
|
81
|
-
"a",
|
|
82
|
-
"an",
|
|
83
|
-
"and",
|
|
84
|
-
"are",
|
|
85
|
-
"as",
|
|
86
|
-
"at",
|
|
87
|
-
"be",
|
|
88
|
-
"by",
|
|
89
|
-
"for",
|
|
90
|
-
"in",
|
|
91
|
-
"is",
|
|
92
|
-
"it",
|
|
93
|
-
"of",
|
|
94
|
-
"on",
|
|
95
|
-
"or",
|
|
96
|
-
"that",
|
|
97
|
-
"the",
|
|
98
|
-
"this",
|
|
99
|
-
"we",
|
|
100
|
-
"with",
|
|
101
|
-
}
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
_DIRECTIVE_RE = re.compile(
|
|
105
|
-
r"^\s*(?:todo|fixme|hack\b|xxx|noqa|sarj-noqa|type:|pragma|pyright|mypy|fmt:|isort|ruff|"
|
|
106
|
-
r"pylint|flake8|nosec|nosemgrep)",
|
|
107
|
-
re.IGNORECASE,
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def _narrates_value(body: str, code: str) -> bool:
|
|
112
|
-
if not body or _DIRECTIVE_RE.match(body) or has_external_reference(body):
|
|
113
|
-
return False
|
|
114
|
-
code_numbers = {match.group(0) for match in _NUMBER_RE.finditer(code)}
|
|
115
|
-
if not code_numbers:
|
|
116
|
-
return False
|
|
117
|
-
words = [match.group(0).lower() for match in _WORD_RE.finditer(body)]
|
|
118
|
-
if not words:
|
|
119
|
-
return False
|
|
120
|
-
comment_numbers = {match.group(0) for match in _NUMBER_RE.finditer(body)}
|
|
121
|
-
if not comment_numbers or not comment_numbers <= code_numbers:
|
|
122
|
-
return False
|
|
123
|
-
if not any(word in _UNIT_WORDS for word in words):
|
|
124
|
-
return False
|
|
125
|
-
identifiers = code_tokens(code)
|
|
126
|
-
identifier_stems = {stem(token) for token in identifiers}
|
|
127
|
-
for word in words:
|
|
128
|
-
if word in _STOPWORDS or word in _UNIT_WORDS or word in comment_numbers:
|
|
129
|
-
continue
|
|
130
|
-
if word in identifiers or stem(word) in identifier_stems:
|
|
131
|
-
continue
|
|
132
|
-
return False
|
|
133
|
-
return True
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
class TrailingValueNarration(Rule):
|
|
137
|
-
id: str = "trailing-value-narration"
|
|
138
|
-
code: str = "SARJ051"
|
|
139
|
-
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
140
|
-
summary="Trailing comment restates a literal value and its unit.",
|
|
141
|
-
rationale="A unit encoded only in a comment can drift from the value and is unavailable to type checking.",
|
|
142
|
-
remediation="Encode the unit in the name or value type, such as timeout_seconds or timedelta.",
|
|
143
|
-
category=RuleCategory.MAINTAINABILITY,
|
|
144
|
-
autofix=AutofixPolicy.SUGGESTION,
|
|
145
|
-
limitations=(
|
|
146
|
-
"Detection targets simple numeric assignments with a trailing comment that repeats the number and unit.",
|
|
147
|
-
"Approximate conversions, reasons, references, directives, bracketed values, and generated files are excluded.",
|
|
148
|
-
),
|
|
149
|
-
examples=(
|
|
150
|
-
RuleExample(
|
|
151
|
-
example_id="repeated-duration",
|
|
152
|
-
title="Comment repeats the duration",
|
|
153
|
-
outcome=ExampleOutcome.MATCH,
|
|
154
|
-
files=(ExampleFile.python("settings.py", "STALE_TIME = 5 * 60 * 1000 # 5 minutes\n"),),
|
|
155
|
-
focus_path=PurePosixPath("settings.py"),
|
|
156
|
-
expected_count=1,
|
|
157
|
-
public=True,
|
|
158
|
-
),
|
|
159
|
-
RuleExample(
|
|
160
|
-
example_id="comment-explains-policy",
|
|
161
|
-
title="Comment explains a policy",
|
|
162
|
-
outcome=ExampleOutcome.NO_MATCH,
|
|
163
|
-
files=(ExampleFile.python("settings.py", "BACKOFF = 2 * 60 # doubles per attempt\n"),),
|
|
164
|
-
focus_path=PurePosixPath("settings.py"),
|
|
165
|
-
expected_count=0,
|
|
166
|
-
public=True,
|
|
167
|
-
),
|
|
168
|
-
),
|
|
169
|
-
)
|
|
170
|
-
description: str = documentation.summary
|
|
171
|
-
|
|
172
|
-
@override
|
|
173
|
-
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
174
|
-
if is_generated(path, source):
|
|
175
|
-
return []
|
|
176
|
-
try:
|
|
177
|
-
trailing = trailing_comments(source)
|
|
178
|
-
nested = nested_comment_lines(source)
|
|
179
|
-
except tokenize.TokenError, IndentationError, SyntaxError:
|
|
180
|
-
return []
|
|
181
|
-
lines = source.splitlines()
|
|
182
|
-
diags: list[Diagnostic] = []
|
|
183
|
-
for line, col, body in trailing:
|
|
184
|
-
if line > len(lines) or line in nested:
|
|
185
|
-
continue
|
|
186
|
-
if _narrates_value(body, lines[line - 1][:col]):
|
|
187
|
-
diags.append(
|
|
188
|
-
Diagnostic(
|
|
189
|
-
path=path,
|
|
190
|
-
line=line,
|
|
191
|
-
col=col + 1,
|
|
192
|
-
code=self.code,
|
|
193
|
-
message=self.description,
|
|
194
|
-
column_encoding=ColumnEncoding.CODEPOINTS,
|
|
195
|
-
)
|
|
196
|
-
)
|
|
197
|
-
return diags
|