sarj-python-lint 0.35.0__tar.gz → 0.36.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/PKG-INFO +22 -7
  2. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/README.md +21 -6
  3. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/pyproject.toml +1 -1
  4. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/_registry.py +4 -0
  5. sarj_python_lint-0.36.0/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +115 -0
  6. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/.gitignore +0 -0
  7. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/__init__.py +0 -0
  8. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/__main__.py +0 -0
  9. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  10. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/_secret_names.py +0 -0
  11. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/_version.py +0 -0
  12. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/py.typed +0 -0
  13. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/ratchet.py +0 -0
  14. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rule_base.py +0 -0
  15. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  16. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  17. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  18. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  19. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  20. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  21. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  22. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  23. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  24. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  25. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/conditional_assertion_in_test.py +0 -0
  26. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  27. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/duplicate_test_body.py +0 -0
  28. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/duplicated_override_docstring.py +0 -0
  29. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
  30. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/inefficient_string_concat_in_loop.py +0 -0
  31. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/interaction_only_test.py +0 -0
  32. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
  33. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/kwonly_same_type_params.py +0 -0
  34. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  35. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
  36. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  37. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  38. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_fat_try_blocks.py +0 -0
  39. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
  40. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -0
  41. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  42. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_fstring_in_log.py +0 -0
  43. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +0 -0
  44. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_implicit_attribute_access.py +0 -0
  45. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
  46. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  47. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
  48. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
  49. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_raw_sql_in_tests.py +0 -0
  50. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  51. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  52. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  53. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  54. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
  55. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_sequential_await.py +0 -0
  56. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_sleep_in_test_body.py +0 -0
  57. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
  58. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  59. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/no_unreachable_after_terminal.py +0 -0
  60. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  61. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/parametrize_case_needs_id.py +0 -0
  62. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  63. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  64. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  65. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  66. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  67. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/prefer_match_pattern_destructuring.py +0 -0
  68. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  69. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  70. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  71. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  72. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  73. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/prefer_real_store_in_tests.py +0 -0
  74. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  75. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  76. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  77. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
  78. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  79. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  80. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  81. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
  82. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  83. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  84. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  85. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/single_public_export.py +0 -0
  86. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/sleep_with_computed_arg_in_test.py +0 -0
  87. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  88. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  89. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/tautological_mock_assertion.py +0 -0
  90. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/test_loops_over_literal_cases.py +0 -0
  91. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  92. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
  93. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  94. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/xfail_requires_strict.py +0 -0
  95. {sarj_python_lint-0.35.0 → sarj_python_lint-0.36.0}/src/sarj_python_lint/rules/zero_assertion_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sarj-python-lint
3
- Version: 0.35.0
3
+ Version: 0.36.0
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://github.com/sarj-ai/standards/tree/main/packages/python
6
6
  Project-URL: Repository, https://github.com/sarj-ai/standards
@@ -137,12 +137,27 @@ SARJ050 flags none of them. The remedy deletes the section and keeps the
137
137
  summary, which was checked against the shipped strict config: ruff's D417 does
138
138
  not fire on a docstring with no parameter section.
139
139
 
140
- The sibling `Returns:` shape was measured and **rejected**: deleting a
141
- `Returns:` section makes DOC201 fire, so the only compliant remedy is deleting a
142
- docstring whose summary may be the valuable part. Two more were rejected on
143
- volume — property docstrings restating the property name and reST/epydoc type
144
- duplication (`:type x: int`, `:rtype:`) both measure **0** first-party findings
145
- outside generated code.
140
+ Two more shapes were rejected on volume property docstrings restating the
141
+ property name and reST/epydoc type duplication (`:type x: int`, `:rtype:`) both
142
+ measure **0** first-party findings outside generated code.
143
+
144
+ ### The `Returns:` half (0.36.0)
145
+
146
+ ```yaml
147
+ - id: sarj-docstring-returns-restate-signature # SARJ087
148
+ ```
149
+
150
+ `SARJ087` is the `Returns:` sibling of SARJ086, and it was **rejected once**:
151
+ deleting a `Returns:` section used to make ruff's DOC201 fire, so the only
152
+ compliant remedy was deleting the whole docstring. #164 then removed DOC201 from
153
+ `ruff.strict.toml` as a rule that DEMANDS prose, and the premise expired — under
154
+ the shipped config the section goes and the summary stays.
155
+
156
+ 756 findings over 33 OSS repos / 35,254 files; two seeded samples of 40 and 20
157
+ read against source gave **~2%** false positives, the whole of which was one
158
+ family (`Returns: A new X` — whether the value is a copy is the one thing
159
+ `-> Self` cannot say) now guarded. Three findings on this repo's own source, all
160
+ true, all deleted. Measurements: [docs/rules/SARJ087.md](../../docs/rules/SARJ087.md).
146
161
 
147
162
  ### House conventions moved out of consumer repos (0.21.0)
148
163
 
@@ -119,12 +119,27 @@ SARJ050 flags none of them. The remedy deletes the section and keeps the
119
119
  summary, which was checked against the shipped strict config: ruff's D417 does
120
120
  not fire on a docstring with no parameter section.
121
121
 
122
- The sibling `Returns:` shape was measured and **rejected**: deleting a
123
- `Returns:` section makes DOC201 fire, so the only compliant remedy is deleting a
124
- docstring whose summary may be the valuable part. Two more were rejected on
125
- volume — property docstrings restating the property name and reST/epydoc type
126
- duplication (`:type x: int`, `:rtype:`) both measure **0** first-party findings
127
- outside generated code.
122
+ Two more shapes were rejected on volume property docstrings restating the
123
+ property name and reST/epydoc type duplication (`:type x: int`, `:rtype:`) both
124
+ measure **0** first-party findings outside generated code.
125
+
126
+ ### The `Returns:` half (0.36.0)
127
+
128
+ ```yaml
129
+ - id: sarj-docstring-returns-restate-signature # SARJ087
130
+ ```
131
+
132
+ `SARJ087` is the `Returns:` sibling of SARJ086, and it was **rejected once**:
133
+ deleting a `Returns:` section used to make ruff's DOC201 fire, so the only
134
+ compliant remedy was deleting the whole docstring. #164 then removed DOC201 from
135
+ `ruff.strict.toml` as a rule that DEMANDS prose, and the premise expired — under
136
+ the shipped config the section goes and the summary stays.
137
+
138
+ 756 findings over 33 OSS repos / 35,254 files; two seeded samples of 40 and 20
139
+ read against source gave **~2%** false positives, the whole of which was one
140
+ family (`Returns: A new X` — whether the value is a copy is the one thing
141
+ `-> Self` cannot say) now guarded. Three findings on this repo's own source, all
142
+ true, all deleted. Measurements: [docs/rules/SARJ087.md](../../docs/rules/SARJ087.md).
128
143
 
129
144
  ### House conventions moved out of consumer repos (0.21.0)
130
145
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "0.35.0"
3
+ version = "0.36.0"
4
4
  description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "sarj-ai" }]
@@ -6,6 +6,9 @@ from sarj_python_lint.rules.conditional_assertion_in_test import ConditionalAsse
6
6
  from sarj_python_lint.rules.docstring_args_restate_signature import (
7
7
  DocstringArgsRestateSignature,
8
8
  )
9
+ from sarj_python_lint.rules.docstring_returns_restate_signature import (
10
+ DocstringReturnsRestateSignature,
11
+ )
9
12
  from sarj_python_lint.rules.duplicate_test_body import DuplicateTestBody
10
13
  from sarj_python_lint.rules.duplicated_override_docstring import (
11
14
  DuplicatedOverrideDocstring,
@@ -184,6 +187,7 @@ REGISTRY: dict[str, type[Rule]] = {
184
187
  DuplicatedOverrideDocstring.id: DuplicatedOverrideDocstring,
185
188
  RedundantClassDocstring.id: RedundantClassDocstring,
186
189
  DocstringArgsRestateSignature.id: DocstringArgsRestateSignature,
190
+ DocstringReturnsRestateSignature.id: DocstringReturnsRestateSignature,
187
191
  }
188
192
 
189
193
  __all__ = ["REGISTRY"]
@@ -0,0 +1,115 @@
1
+ """SARJ087 — A `Returns:` block that only re-spells the name and the return annotation.
2
+
3
+ Examples: https://github.com/sarj-ai/standards/blob/main/packages/python/tests/rules/test_docstring_returns_restate_signature.py
4
+ Evidence: https://github.com/sarj-ai/standards/blob/main/docs/rules/SARJ087.md
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import ast
10
+ import re
11
+ from typing import TYPE_CHECKING, override
12
+
13
+ from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
14
+ from sarj_python_lint.rules._ast_index import children
15
+ from sarj_python_lint.rules._comments import is_protected
16
+ from sarj_python_lint.rules._docstrings import (
17
+ PROMPT_DECORATOR_MARKERS,
18
+ VALUE_MARKER_RE,
19
+ decorator_markers,
20
+ restates,
21
+ sections,
22
+ signature_stems,
23
+ )
24
+ from sarj_python_lint.rules._paths import is_generated
25
+
26
+
27
+ if TYPE_CHECKING:
28
+ from pathlib import Path
29
+
30
+
31
+ _RETURN_SECTIONS = ("Returns", "Return", "Yields", "Yield")
32
+
33
+ # Identity semantics: whether the value handed back is a FRESH object or the
34
+ # receiver itself is the one fact `-> Self` and `-> Foo` cannot carry, and the
35
+ # words that state it (`new`, `copy`, `same`) are stopwords for the restatement
36
+ # tokenizer, so the block reads as pure ceremony without this.
37
+ _IDENTITY_RE = re.compile(
38
+ r"\b(?:new|copy|copies|copied|clone[ds]?|fresh|same|itself|self|shallow|deep|"
39
+ r"in[- ]place|unchanged|original)\b",
40
+ re.IGNORECASE,
41
+ )
42
+
43
+
44
+ def _return_block(docstring: str) -> str | None:
45
+ found = sections(docstring)
46
+ for name in _RETURN_SECTIONS:
47
+ if name in found:
48
+ return found[name]
49
+ return None
50
+
51
+
52
+ class DocstringReturnsRestateSignature(Rule):
53
+ id: str = "docstring-returns-restate-signature"
54
+ code: str = "SARJ087"
55
+ has_evidence: bool = True
56
+ description: str = (
57
+ "`Returns:` block adds nothing the name and the return annotation do not "
58
+ "already say — delete the section and keep the summary."
59
+ )
60
+
61
+ @override
62
+ def check(self, path: Path, source: str) -> list[Diagnostic]:
63
+ if is_generated(path, source):
64
+ return []
65
+ tree = parse_or_none(path, source)
66
+ if tree is None:
67
+ return []
68
+ diags: list[Diagnostic] = []
69
+ self._walk(tree, None, path, diags)
70
+ return sorted(diags, key=lambda diag: diag.line)
71
+
72
+ def _walk(self, node: ast.AST, class_name: str | None, path: Path, diags: list[Diagnostic]) -> None:
73
+ for child in children(node):
74
+ if isinstance(child, ast.FunctionDef | ast.AsyncFunctionDef):
75
+ self._check_function(child, class_name, path, diags)
76
+ self._walk(child, class_name, path, diags)
77
+ elif isinstance(child, ast.ClassDef):
78
+ self._walk(child, child.name, path, diags)
79
+ else:
80
+ self._walk(child, class_name, path, diags)
81
+
82
+ def _check_function(
83
+ self,
84
+ node: ast.FunctionDef | ast.AsyncFunctionDef,
85
+ class_name: str | None,
86
+ path: Path,
87
+ diags: list[Diagnostic],
88
+ ) -> None:
89
+ docstring = ast.get_docstring(node, clean=True)
90
+ if not docstring:
91
+ return
92
+ block = _return_block(docstring)
93
+ if block is None:
94
+ return
95
+ if VALUE_MARKER_RE.search(block) or is_protected(block) or _IDENTITY_RE.search(block):
96
+ return
97
+ if decorator_markers(node) & PROMPT_DECORATOR_MARKERS:
98
+ return
99
+ stems = signature_stems(node, class_name)
100
+ # The whole-docstring case is SARJ050's; reporting it here too would
101
+ # make one deletion look like two findings.
102
+ if restates(docstring, stems):
103
+ return
104
+ if not restates(block, stems):
105
+ return
106
+ expr = node.body[0]
107
+ diags.append(
108
+ Diagnostic(
109
+ path=path,
110
+ line=expr.lineno,
111
+ col=expr.col_offset + 1,
112
+ code=self.code,
113
+ message=self.description,
114
+ )
115
+ )