sarj-python-lint 0.73.0__tar.gz → 0.73.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/PKG-INFO +2 -2
  2. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/pyproject.toml +5 -5
  3. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +4 -1
  4. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +4 -1
  5. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/duplicated_override_docstring.py +4 -1
  6. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_comment_cruft.py +6 -2
  7. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_long_comment.py +8 -2
  8. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_restated_comment.py +8 -2
  9. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_typed_doc_sections.py +4 -1
  10. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +154 -13
  11. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/redundant_class_docstring.py +8 -2
  12. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/redundant_docstring.py +8 -2
  13. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/redundant_module_docstring.py +8 -2
  14. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/restated_test_docstring.py +4 -1
  15. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/.gitignore +0 -0
  16. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/LICENSE +0 -0
  17. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/README.md +0 -0
  18. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/__init__.py +0 -0
  19. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/__main__.py +0 -0
  20. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/_filesystem.py +0 -0
  21. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  22. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/_secret_names.py +0 -0
  23. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/_version.py +0 -0
  24. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/py.typed +0 -0
  25. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/ratchet.py +0 -0
  26. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rule_base.py +0 -0
  27. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/__init__.py +0 -0
  28. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  29. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/_comments.py +0 -0
  30. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  31. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  32. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/_first_party.py +0 -0
  33. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/_imports.py +0 -0
  34. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/_logging.py +0 -0
  35. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/_paths.py +0 -0
  36. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/_project_index.py +0 -0
  37. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  38. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/_pytest.py +0 -0
  39. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/_registry.py +0 -0
  40. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/_sql.py +0 -0
  41. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  42. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  43. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/created_at_order_requires_tiebreaker.py +0 -0
  44. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/defect_xfail_requires_strict.py +0 -0
  45. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/duplicate_test_body.py +0 -0
  46. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/fastapi_openapi_contract.py +0 -0
  47. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
  48. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  49. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  50. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
  51. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  52. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  53. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
  54. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  55. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  56. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
  57. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -0
  58. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  59. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  60. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +0 -0
  61. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  62. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  63. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
  64. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  65. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
  66. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
  67. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  68. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  69. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  70. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
  71. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
  72. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  73. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  74. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  75. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  76. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  77. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  78. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  79. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  80. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  81. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  82. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  83. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  84. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  85. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  86. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  87. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  88. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  89. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  90. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  91. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  92. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  93. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  94. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  95. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
  96. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  97. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  98. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  99. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/preserve_declared_nominal_id.py +0 -0
  100. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/preserve_enum_types.py +0 -0
  101. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  102. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
  103. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  104. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  105. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  106. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  107. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  108. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/require_validated_row_factory.py +0 -0
  109. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/source_coupled_test.py +0 -0
  110. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/sql_requires_injected_pool_owner.py +0 -0
  111. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/stepdown.py +0 -0
  112. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  113. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  114. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
  115. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  116. {sarj_python_lint-0.73.0 → sarj_python_lint-0.73.2}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 0.73.0
3
+ Version: 0.73.2
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
6
6
  Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "0.73.0"
3
+ version = "0.73.2"
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" }]
@@ -30,13 +30,13 @@ Issues = "https://github.com/sarj-ai/standards/issues"
30
30
 
31
31
  [dependency-groups]
32
32
  dev = [
33
- "pytest>=9.0",
34
- "ruff>=0.16.1",
35
- "basedpyright>=1.39",
33
+ "pytest>=9.1.1",
34
+ "ruff>=0.16.3",
35
+ "basedpyright>=1.39.10",
36
36
  ]
37
37
 
38
38
  [build-system]
39
- requires = ["hatchling==1.31.0"]
39
+ requires = ["hatchling==1.32.0"]
40
40
  build-backend = "hatchling.build"
41
41
 
42
42
  [tool.hatch.build.targets.wheel]
@@ -40,7 +40,10 @@ class DocstringArgsRestateSignature(Rule):
40
40
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
41
41
  summary="Argument documentation must add facts beyond the function signature.",
42
42
  rationale="Repeating parameter names and types obscures useful behavioral contracts and drifts when signatures change.",
43
- remediation="Remove the redundant argument section, or retain it only to document constraints, units, defaults, or semantics absent from the signature.",
43
+ remediation=(
44
+ "Delete the human-only docstring or redundant argument section. Express author-controlled semantics with "
45
+ "names and types; keep hidden constraints or units as a concise local comment."
46
+ ),
44
47
  category=RuleCategory.MAINTAINABILITY,
45
48
  autofix=AutofixPolicy.NONE,
46
49
  limitations=(
@@ -98,7 +98,10 @@ class DocstringReturnsRestateSignature(Rule):
98
98
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
99
99
  summary="Return documentation must add facts beyond the function name and annotation.",
100
100
  rationale="Repeating the return type or function name adds noise and can become stale without explaining the result's semantics.",
101
- remediation="Remove the redundant return section, or document identity, units, constraints, or other behavior absent from the signature.",
101
+ remediation=(
102
+ "Delete the human-only docstring or redundant return section. Express author-controlled identity and "
103
+ "units with names and types; keep hidden constraints as a concise local comment."
104
+ ),
102
105
  category=RuleCategory.MAINTAINABILITY,
103
106
  autofix=AutofixPolicy.NONE,
104
107
  limitations=(
@@ -47,7 +47,10 @@ class DuplicatedOverrideDocstring(Rule):
47
47
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
48
48
  summary="Remove an override docstring copied verbatim from its local base method.",
49
49
  rationale="Inherited documentation is already discoverable, while a duplicate adds a second copy that can drift.",
50
- remediation="Delete the copied docstring, or rewrite it only when the override has behavior-specific information to add.",
50
+ remediation=(
51
+ "Delete the copied docstring. If author-controlled override code is unclear, clarify names or extract a "
52
+ "helper; keep behavior-specific differences as a concise comment near the divergent code."
53
+ ),
51
54
  category=RuleCategory.MAINTAINABILITY,
52
55
  autofix=AutofixPolicy.NONE,
53
56
  limitations=(
@@ -450,7 +450,10 @@ class NoCommentCruft(Rule):
450
450
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
451
451
  summary="Comment repeats code, preserves dead code, or adds a decorative section marker.",
452
452
  rationale="Mechanical narration and dead code obscure the constraints and rationale that comments should preserve.",
453
- remediation="Delete the cruft and keep only concise comments that explain a non-obvious reason or constraint.",
453
+ remediation=(
454
+ "Delete the cruft. If author-controlled code is unclear without narration, clarify names, types, or "
455
+ "structure; keep only concise comments for a hidden reason or constraint."
456
+ ),
454
457
  category=RuleCategory.MAINTAINABILITY,
455
458
  limitations=(
456
459
  "Only standalone comments are classified; trailing comments, docstrings, directives, and referenced notes are excluded.",
@@ -640,7 +643,8 @@ class NoCommentCruft(Rule):
640
643
  code=self.code,
641
644
  message=(
642
645
  f"File-header comment preamble ({len(leading)} lines) — "
643
- "use a module docstring for the why, not a block of comments."
646
+ "delete the ceremony; use a descriptive module path and place durable constraints near the "
647
+ "code they govern or in maintained documentation."
644
648
  ),
645
649
  column_encoding=ColumnEncoding.CODEPOINTS,
646
650
  )
@@ -33,9 +33,15 @@ class NoLongComment(Rule):
33
33
  id = "no-long-comment"
34
34
  code = "SARJ091"
35
35
  documentation = RuleDocumentation(
36
- summary="Long docstrings must use deliberate documentation structure or technical anchors.",
36
+ summary=(
37
+ "Unstructured docstring prose wall — delete it; clarify author-controlled names, types, and structure or "
38
+ "move durable design context to maintained documentation."
39
+ ),
37
40
  rationale="An unstructured prose wall is difficult to scan and often hides a contract that belongs in clearer code or durable structured documentation.",
38
- remediation="Clarify the code or restructure the docstring with paragraphs, lists, code, paths, links, or other meaningful technical anchors.",
41
+ remediation=(
42
+ "Delete human-only prose and clarify names, types, or structure. Keep machine-consumed documentation; move "
43
+ "broader design context to maintained documentation."
44
+ ),
39
45
  category=RuleCategory.MAINTAINABILITY,
40
46
  autofix=AutofixPolicy.NONE,
41
47
  limitations=(
@@ -217,9 +217,15 @@ class NoRestatedComment(Rule):
217
217
  id: str = "no-restated-comment"
218
218
  code: str = "SARJ049"
219
219
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
220
- summary="Comment restates the statement immediately below it.",
220
+ summary=(
221
+ "Comment restates the next statement — delete it; clarify an author-controlled name or extract a named "
222
+ "helper if the code is unclear."
223
+ ),
221
224
  rationale="Comments that repeat code add reading cost and can become stale without explaining intent.",
222
- remediation="Delete the comment or replace it with context the statement cannot express.",
225
+ remediation=(
226
+ "Delete the comment. If author-controlled code is unclear without it, clarify a name or extract a named "
227
+ "helper; keep comments only for context the statement cannot express."
228
+ ),
223
229
  category=RuleCategory.MAINTAINABILITY,
224
230
  autofix=AutofixPolicy.SUGGESTION,
225
231
  limitations=(
@@ -27,7 +27,10 @@ class NoTypedDocSections(Rule):
27
27
  documentation = RuleDocumentation(
28
28
  summary="Docstring sections must not repeat types already present in a fully typed signature.",
29
29
  rationale="Duplicated type spellings drift from annotations and add noise without strengthening the behavioral contract.",
30
- remediation="Remove the repeated type while retaining behavioral facts, constraints, units, and error conditions.",
30
+ remediation=(
31
+ "Delete the human-only docstring or repeated type. Express author-controlled contracts with names and "
32
+ "annotations; keep hidden constraints, units, or error conditions as a concise local comment."
33
+ ),
31
34
  category=RuleCategory.MAINTAINABILITY,
32
35
  autofix=AutofixPolicy.NONE,
33
36
  limitations=(
@@ -39,6 +39,11 @@ _OPENAI_TOOL_DECORATORS = frozenset({"agents.function_tool"})
39
39
  _PYDANTIC_DOC_DECORATORS = frozenset({"pydantic.computed_field"})
40
40
  _FASTAPI_CONSTRUCTORS = frozenset({"fastapi.APIRouter", "fastapi.FastAPI"})
41
41
  _FASTAPI_ROUTE_METHODS = frozenset({"api_route", "delete", "get", "head", "options", "patch", "post", "put", "trace"})
42
+ _TYPER_CONSTRUCTORS = frozenset({"typer.Typer"})
43
+ _TYPER_DOC_METHODS = frozenset({"callback", "command"})
44
+ _CLICK_DOC_DECORATORS = frozenset({"click.command", "click.group"})
45
+ _CLICK_GROUP_CONSTRUCTORS = frozenset({"click.Group"})
46
+ _CLICK_GROUP_METHODS = frozenset({"command", "group"})
42
47
  _KNOWN_DECORATORS = frozenset(
43
48
  {
44
49
  "builtins.property",
@@ -78,14 +83,18 @@ class NoUnnecessaryDocstring(ProjectRule):
78
83
  id: str = "no-unnecessary-docstring"
79
84
  code: str = "SARJ420"
80
85
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
81
- summary="Keep docstrings only when a machine or framework consumes them.",
86
+ summary=(
87
+ "No docstring consumer detected — delete it; make author-controlled names, types, and structure explain "
88
+ "the code."
89
+ ),
82
90
  rationale=(
83
- "Human-only docstrings duplicate names, signatures, and nearby code while creating a second prose surface "
84
- "that agents expand and maintainers must review."
91
+ "A docstring with no detected consumer makes code depend on prose for ordinary meaning and creates a "
92
+ "second maintenance surface that agents expand and maintainers must review."
85
93
  ),
86
94
  remediation=(
87
- "Delete the docstring; move a genuinely hidden local invariant to one concise comment, or suppress SARJ420 "
88
- "when an external documentation consumer cannot be detected mechanically."
95
+ "Delete the docstring. If author-controlled code is unclear without it, clarify names, types, and structure "
96
+ "or extract a small named helper. Keep a genuinely hidden invariant as one concise local comment. When "
97
+ "external tooling consumes __doc__, use an exact SARJ420 suppression that names the consumer."
89
98
  ),
90
99
  category=RuleCategory.MAINTAINABILITY,
91
100
  autofix=AutofixPolicy.NONE,
@@ -98,6 +107,10 @@ class NoUnnecessaryDocstring(ProjectRule):
98
107
  "The rule is intentionally default-deny: public API documentation without mechanically visible "
99
108
  "consumption needs an auditable SARJ420 suppression."
100
109
  ),
110
+ (
111
+ "Dynamic Click or Typer decorator options are conservatively treated as possible help consumers; "
112
+ "explicit non-None help leaves the docstring eligible."
113
+ ),
101
114
  ),
102
115
  examples=(
103
116
  RuleExample(
@@ -143,9 +156,13 @@ class NoUnnecessaryDocstring(ProjectRule):
143
156
  explicitly_consumed = _explicit_docstring_consumers(tree, frozenset(owner_keys.values()))
144
157
  shadowed = _module_bound_names(tree)
145
158
  imports = _import_bindings(tree, shadowed)
146
- fastapi_consumed = _fastapi_docstring_consumers(tree, imports)
147
- schema_consumed = _schema_docstring_consumers(tree, imports)
148
- project_schema_consumed = _project_schema_docstring_consumers(self._project_indexes, path, tree)
159
+ consumed_owner_ids = (
160
+ _fastapi_docstring_consumers(tree, imports)
161
+ | _typer_docstring_consumers(tree, imports)
162
+ | _click_group_docstring_consumers(tree, imports)
163
+ | _schema_docstring_consumers(tree, imports)
164
+ | _project_schema_docstring_consumers(self._project_indexes, path, tree)
165
+ )
149
166
  source_lines = source.splitlines()
150
167
  diagnostics: list[Diagnostic] = []
151
168
  for owner in _docstring_owners(tree):
@@ -159,9 +176,7 @@ class NoUnnecessaryDocstring(ProjectRule):
159
176
  owner_imports, owner_shadowed = _module_environment_before(tree, owner_line)
160
177
  if (
161
178
  _is_syntax_required(owner)
162
- or id(owner) in fastapi_consumed
163
- or id(owner) in schema_consumed
164
- or id(owner) in project_schema_consumed
179
+ or id(owner) in consumed_owner_ids
165
180
  or _framework_consumes_docstring(owner, owner_imports, owner_shadowed)
166
181
  ):
167
182
  continue
@@ -406,7 +421,9 @@ def _framework_consumes_docstring(
406
421
  return False
407
422
  for decorator in owner.decorator_list:
408
423
  target = decorator.func if isinstance(decorator, ast.Call) else decorator
409
- name = _resolve_imported_name(target, imports)
424
+ dotted = _dotted_name(target)
425
+ head = None if dotted is None else dotted.partition(".")[0]
426
+ name = _resolve_imported_name(target, imports) if head in imports else None
410
427
  if name in _LIVEKIT_TOOL_DECORATORS and (
411
428
  not isinstance(decorator, ast.Call) or _livekit_uses_docstring(decorator)
412
429
  ):
@@ -419,9 +436,11 @@ def _framework_consumes_docstring(
419
436
  not isinstance(decorator, ast.Call) or _keyword_is_missing_or_none(decorator, "description")
420
437
  ):
421
438
  return True
439
+ if name in _CLICK_DOC_DECORATORS and (not isinstance(decorator, ast.Call) or _doc_help_is_implicit(decorator)):
440
+ return True
422
441
  if name in _KNOWN_DECORATORS:
423
442
  return True
424
- if name == "property" and "property" not in shadowed:
443
+ if dotted == "property" and "property" not in shadowed:
425
444
  return True
426
445
  if not isinstance(owner, ast.ClassDef):
427
446
  return False
@@ -493,6 +512,10 @@ def _keyword_is_missing_or_none(call: ast.Call, name: str) -> bool:
493
512
  return value is None or (isinstance(value, ast.Constant) and value.value is None)
494
513
 
495
514
 
515
+ def _doc_help_is_implicit(call: ast.Call) -> bool:
516
+ return _keyword_is_missing_or_none(call, "help")
517
+
518
+
496
519
  def _livekit_uses_docstring(call: ast.Call) -> bool:
497
520
  return not any(
498
521
  keyword.arg == "raw_schema" and not (isinstance(keyword.value, ast.Constant) and keyword.value.value is None)
@@ -540,6 +563,77 @@ def _fastapi_docstring_consumers(tree: ast.Module, imports: dict[str, str]) -> f
540
563
  return frozenset(consumed)
541
564
 
542
565
 
566
+ def _typer_docstring_consumers(tree: ast.Module, imports: dict[str, str]) -> frozenset[int]:
567
+ consumed: set[int] = set()
568
+
569
+ def visit(body: list[ast.stmt], inherited: dict[str, bool]) -> None:
570
+ bindings = dict(inherited)
571
+ for statement in body:
572
+ if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
573
+ if any(_is_typer_callback(decorator, bindings) for decorator in statement.decorator_list):
574
+ consumed.add(id(statement))
575
+ nested = dict(bindings)
576
+ if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef)):
577
+ for argument in (
578
+ *statement.args.posonlyargs,
579
+ *statement.args.args,
580
+ *statement.args.kwonlyargs,
581
+ ):
582
+ nested[argument.arg] = (
583
+ argument.annotation is not None
584
+ and _resolve_imported_name(argument.annotation, imports) in _TYPER_CONSTRUCTORS
585
+ )
586
+ if statement.args.vararg is not None:
587
+ nested[statement.args.vararg.arg] = False
588
+ if statement.args.kwarg is not None:
589
+ nested[statement.args.kwarg.arg] = False
590
+ visit(statement.body, nested)
591
+ bindings[statement.name] = False
592
+ continue
593
+ if isinstance(statement, (ast.Assign, ast.AnnAssign)):
594
+ targets = statement.targets if isinstance(statement, ast.Assign) else [statement.target]
595
+ value = statement.value
596
+ is_receiver = _is_typer_receiver_value(value, bindings, imports)
597
+ for target in targets:
598
+ if isinstance(target, ast.Name):
599
+ bindings[target.id] = is_receiver
600
+
601
+ visit(tree.body, {})
602
+ return frozenset(consumed)
603
+
604
+
605
+ def _click_group_docstring_consumers(tree: ast.Module, imports: dict[str, str]) -> frozenset[int]:
606
+ consumed: set[int] = set()
607
+
608
+ def visit(body: list[ast.stmt], inherited: dict[str, bool]) -> None:
609
+ bindings = dict(inherited)
610
+ for statement in body:
611
+ if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
612
+ group_decorator = next(
613
+ (
614
+ decorator
615
+ for decorator in statement.decorator_list
616
+ if _is_click_group_decorator(decorator, imports)
617
+ ),
618
+ None,
619
+ )
620
+ if any(_is_click_group_callback(decorator, bindings) for decorator in statement.decorator_list):
621
+ consumed.add(id(statement))
622
+ visit(statement.body, bindings)
623
+ bindings[statement.name] = group_decorator is not None
624
+ continue
625
+ if isinstance(statement, (ast.Assign, ast.AnnAssign)):
626
+ targets = statement.targets if isinstance(statement, ast.Assign) else [statement.target]
627
+ value = statement.value
628
+ is_receiver = _is_click_group_receiver_value(value, bindings, imports)
629
+ for target in targets:
630
+ if isinstance(target, ast.Name):
631
+ bindings[target.id] = is_receiver
632
+
633
+ visit(tree.body, {})
634
+ return frozenset(consumed)
635
+
636
+
543
637
  def _schema_docstring_consumers(tree: ast.Module, imports: dict[str, str]) -> frozenset[int]:
544
638
  classes = {statement.name: statement for statement in ast.walk(tree) if isinstance(statement, ast.ClassDef)}
545
639
  consumed: set[str] = set()
@@ -594,6 +688,53 @@ def _is_fastapi_route(decorator: ast.expr, bindings: dict[str, bool]) -> bool:
594
688
  return description is None or (isinstance(description, ast.Constant) and not description.value)
595
689
 
596
690
 
691
+ def _is_typer_receiver_value(node: ast.AST | None, bindings: dict[str, bool], imports: dict[str, str]) -> bool:
692
+ if isinstance(node, ast.Name):
693
+ return bindings.get(node.id, False) or _resolve_imported_name(node, imports) in _TYPER_CONSTRUCTORS
694
+ return isinstance(node, ast.Call) and (
695
+ _resolve_imported_name(node.func, imports) in _TYPER_CONSTRUCTORS
696
+ or (isinstance(node.func, ast.Name) and bindings.get(node.func.id, False))
697
+ )
698
+
699
+
700
+ def _is_typer_callback(decorator: ast.expr, bindings: dict[str, bool]) -> bool:
701
+ if not isinstance(decorator, ast.Call) or not isinstance(decorator.func, ast.Attribute):
702
+ return False
703
+ receiver = decorator.func.value
704
+ return (
705
+ isinstance(receiver, ast.Name)
706
+ and bindings.get(receiver.id, False)
707
+ and decorator.func.attr in _TYPER_DOC_METHODS
708
+ and _doc_help_is_implicit(decorator)
709
+ )
710
+
711
+
712
+ def _is_click_group_decorator(decorator: ast.expr, imports: dict[str, str]) -> bool:
713
+ target = decorator.func if isinstance(decorator, ast.Call) else decorator
714
+ return _resolve_imported_name(target, imports) == "click.group"
715
+
716
+
717
+ def _is_click_group_receiver_value(node: ast.AST | None, bindings: dict[str, bool], imports: dict[str, str]) -> bool:
718
+ if isinstance(node, ast.Name):
719
+ return bindings.get(node.id, False) or _resolve_imported_name(node, imports) in _CLICK_GROUP_CONSTRUCTORS
720
+ return isinstance(node, ast.Call) and (
721
+ _resolve_imported_name(node.func, imports) in _CLICK_GROUP_CONSTRUCTORS
722
+ or (isinstance(node.func, ast.Name) and bindings.get(node.func.id, False))
723
+ )
724
+
725
+
726
+ def _is_click_group_callback(decorator: ast.expr, bindings: dict[str, bool]) -> bool:
727
+ if not isinstance(decorator, ast.Call) or not isinstance(decorator.func, ast.Attribute):
728
+ return False
729
+ receiver = decorator.func.value
730
+ return (
731
+ isinstance(receiver, ast.Name)
732
+ and bindings.get(receiver.id, False)
733
+ and decorator.func.attr in _CLICK_GROUP_METHODS
734
+ and _doc_help_is_implicit(decorator)
735
+ )
736
+
737
+
597
738
  def _module_bound_names(tree: ast.Module) -> frozenset[str]:
598
739
  names: set[str] = set()
599
740
  for statement in tree.body:
@@ -35,9 +35,15 @@ class RedundantClassDocstring(Rule):
35
35
  id: str = "redundant-class-docstring"
36
36
  code: str = "SARJ085"
37
37
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
38
- summary="Class docstrings must add information beyond the class name and bases.",
38
+ summary=(
39
+ "Class docstring restates the declaration — delete it; clarify author-controlled names, fields, or types "
40
+ "if the role is unclear."
41
+ ),
39
42
  rationale="Restating a class declaration adds maintenance cost without helping a reader understand its contract.",
40
- remediation="Delete the redundant docstring or document an invariant, lifetime, exclusion, or other fact absent from the declaration.",
43
+ remediation=(
44
+ "Delete the docstring and clarify author-controlled names, fields, or types. Keep a hidden invariant, "
45
+ "lifetime, or exclusion as a concise comment near its enforcement."
46
+ ),
41
47
  category=RuleCategory.MAINTAINABILITY,
42
48
  autofix=AutofixPolicy.NONE,
43
49
  limitations=(
@@ -63,9 +63,15 @@ class RedundantDocstring(Rule):
63
63
  id: str = "redundant-docstring"
64
64
  code: str = "SARJ050"
65
65
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
66
- summary="Docstring only restates the signature — delete the whole docstring or document behavior callers cannot infer.",
66
+ summary=(
67
+ "Docstring only restates the signature — delete the whole docstring; clarify author-controlled names and "
68
+ "types if the contract is unclear."
69
+ ),
67
70
  rationale="Restating a clear name and signature creates maintenance work without helping callers.",
68
- remediation="Delete the docstring or document behavior, constraints, side effects, or failure modes not evident from the signature.",
71
+ remediation=(
72
+ "Delete the docstring and clarify author-controlled names, types, or structure. Keep a concise local "
73
+ "comment only for a hidden constraint, side effect, or failure mode."
74
+ ),
69
75
  category=RuleCategory.MAINTAINABILITY,
70
76
  autofix=AutofixPolicy.SUGGESTION,
71
77
  limitations=(
@@ -55,9 +55,15 @@ class RedundantModuleDocstring(Rule):
55
55
  id: str = "redundant-module-docstring"
56
56
  code: str = "SARJ099"
57
57
  documentation = RuleDocumentation(
58
- summary="Module docstrings must add information beyond the file path.",
58
+ summary=(
59
+ "Module docstring restates the file path — delete it; clarify the author-controlled module path or exports "
60
+ "if the purpose is unclear."
61
+ ),
59
62
  rationale="A one-line restatement of a module path duplicates information already visible to readers and search tools.",
60
- remediation="Delete the redundant docstring or document an invariant, boundary, consumer, or compatibility constraint.",
63
+ remediation=(
64
+ "Delete the docstring and clarify the author-controlled module path or exports. Keep durable boundaries "
65
+ "and compatibility constraints near the code they govern or in maintained documentation."
66
+ ),
61
67
  category=RuleCategory.MAINTAINABILITY,
62
68
  autofix=AutofixPolicy.NONE,
63
69
  limitations=(
@@ -128,7 +128,10 @@ class RestatedTestDocstring(Rule):
128
128
  documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
129
129
  summary="Test docstrings must add information beyond the test name and body.",
130
130
  rationale="A docstring that narrates visible test code creates duplicate prose that can drift without explaining the regression or contract.",
131
- remediation="Delete the redundant docstring, improve the test name, or document a reason or constraint not visible in the test.",
131
+ remediation=(
132
+ "Delete the docstring and put the scenario and expected outcome in the test name. Keep a hidden regression "
133
+ "reason or constraint as one concise local comment."
134
+ ),
132
135
  category=RuleCategory.TESTING,
133
136
  autofix=AutofixPolicy.NONE,
134
137
  limitations=(