sarj-python-lint 0.32.0__tar.gz → 0.33.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/PKG-INFO +43 -1
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/README.md +42 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/pyproject.toml +1 -1
- sarj_python_lint-0.33.0/src/sarj_python_lint/rules/_docstrings.py +317 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/_registry.py +10 -0
- sarj_python_lint-0.33.0/src/sarj_python_lint/rules/docstring_args_restate_signature.py +172 -0
- sarj_python_lint-0.33.0/src/sarj_python_lint/rules/duplicated_override_docstring.py +188 -0
- sarj_python_lint-0.33.0/src/sarj_python_lint/rules/redundant_class_docstring.py +201 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/redundant_docstring.py +25 -170
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/.gitignore +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/conditional_assertion_in_test.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/duplicate_test_body.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/inefficient_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/interaction_only_test.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/kwonly_same_type_params.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_fat_try_blocks.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_fstring_in_log.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_implicit_attribute_access.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_patching_system_under_test.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_raw_sql_in_tests.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_sequential_await.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_sleep_in_test_body.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/no_unreachable_after_terminal.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/prefer_match_pattern_destructuring.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/prefer_real_store_in_tests.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/single_public_export.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/sleep_with_computed_arg_in_test.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/tautological_mock_assertion.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/test_loops_over_literal_cases.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.0}/src/sarj_python_lint/rules/xfail_requires_strict.py +0 -0
- {sarj_python_lint-0.32.0 → sarj_python_lint-0.33.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.
|
|
3
|
+
Version: 0.33.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
|
|
@@ -102,6 +102,48 @@ each guard was built from are recorded in the rule module docstrings.
|
|
|
102
102
|
`redundant-docstring` finds real volume on a codebase that has never had it
|
|
103
103
|
(105 in noura-be), so the same baseline ratchet applies.
|
|
104
104
|
|
|
105
|
+
### Docstring-ceremony rules (0.31.0)
|
|
106
|
+
|
|
107
|
+
SARJ050 tests a *function* docstring against its *own signature*. That leaves
|
|
108
|
+
three shapes it cannot reach, each now its own code so a consumer can baseline
|
|
109
|
+
them separately:
|
|
110
|
+
|
|
111
|
+
```yaml
|
|
112
|
+
- id: sarj-duplicated-override-docstring # SARJ084
|
|
113
|
+
- id: sarj-redundant-class-docstring # SARJ085
|
|
114
|
+
- id: sarj-docstring-args-restate-signature # SARJ086
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`SARJ084` flags an override whose docstring is **byte-identical** to the base
|
|
118
|
+
method's, with the base resolved by undotted name inside the same file. There is
|
|
119
|
+
no judgement call — the test is byte equality — and `inspect.getdoc`, `help()`,
|
|
120
|
+
Sphinx and editor hovers all walk the MRO, so deleting the copy changes nothing
|
|
121
|
+
a reader sees. 49 first-party findings, 49 true positives; 137 across 14 OSS
|
|
122
|
+
repos, 18 sampled and read, 0 false positives.
|
|
123
|
+
|
|
124
|
+
`SARJ085` flags a class docstring that only re-spells the class name — the case
|
|
125
|
+
SARJ050's walker structurally never inspects. Its largest guard is that anything
|
|
126
|
+
whose docstring becomes a **published schema description** (pydantic models,
|
|
127
|
+
enums, `TypedDict`s, `@strawberry.type`) is exempt: that string is emitted as
|
|
128
|
+
the JSON-Schema `description` and reaches OpenAPI documents and LLM tool
|
|
129
|
+
schemas. The exemption costs 28 of 34 first-party findings and is not
|
|
130
|
+
negotiable.
|
|
131
|
+
|
|
132
|
+
`SARJ086` flags an `Args:` block whose every entry only re-spells its own
|
|
133
|
+
parameter. It fires where SARJ050 cannot: the header word "args" is a content
|
|
134
|
+
word no signature contains, so *any* `Args:` block makes a docstring
|
|
135
|
+
permanently unflaggable by SARJ050 — 126 first-party functions carry one and
|
|
136
|
+
SARJ050 flags none of them. The remedy deletes the section and keeps the
|
|
137
|
+
summary, which was checked against the shipped strict config: ruff's D417 does
|
|
138
|
+
not fire on a docstring with no parameter section.
|
|
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.
|
|
146
|
+
|
|
105
147
|
### House conventions moved out of consumer repos (0.21.0)
|
|
106
148
|
|
|
107
149
|
```yaml
|
|
@@ -84,6 +84,48 @@ each guard was built from are recorded in the rule module docstrings.
|
|
|
84
84
|
`redundant-docstring` finds real volume on a codebase that has never had it
|
|
85
85
|
(105 in noura-be), so the same baseline ratchet applies.
|
|
86
86
|
|
|
87
|
+
### Docstring-ceremony rules (0.31.0)
|
|
88
|
+
|
|
89
|
+
SARJ050 tests a *function* docstring against its *own signature*. That leaves
|
|
90
|
+
three shapes it cannot reach, each now its own code so a consumer can baseline
|
|
91
|
+
them separately:
|
|
92
|
+
|
|
93
|
+
```yaml
|
|
94
|
+
- id: sarj-duplicated-override-docstring # SARJ084
|
|
95
|
+
- id: sarj-redundant-class-docstring # SARJ085
|
|
96
|
+
- id: sarj-docstring-args-restate-signature # SARJ086
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`SARJ084` flags an override whose docstring is **byte-identical** to the base
|
|
100
|
+
method's, with the base resolved by undotted name inside the same file. There is
|
|
101
|
+
no judgement call — the test is byte equality — and `inspect.getdoc`, `help()`,
|
|
102
|
+
Sphinx and editor hovers all walk the MRO, so deleting the copy changes nothing
|
|
103
|
+
a reader sees. 49 first-party findings, 49 true positives; 137 across 14 OSS
|
|
104
|
+
repos, 18 sampled and read, 0 false positives.
|
|
105
|
+
|
|
106
|
+
`SARJ085` flags a class docstring that only re-spells the class name — the case
|
|
107
|
+
SARJ050's walker structurally never inspects. Its largest guard is that anything
|
|
108
|
+
whose docstring becomes a **published schema description** (pydantic models,
|
|
109
|
+
enums, `TypedDict`s, `@strawberry.type`) is exempt: that string is emitted as
|
|
110
|
+
the JSON-Schema `description` and reaches OpenAPI documents and LLM tool
|
|
111
|
+
schemas. The exemption costs 28 of 34 first-party findings and is not
|
|
112
|
+
negotiable.
|
|
113
|
+
|
|
114
|
+
`SARJ086` flags an `Args:` block whose every entry only re-spells its own
|
|
115
|
+
parameter. It fires where SARJ050 cannot: the header word "args" is a content
|
|
116
|
+
word no signature contains, so *any* `Args:` block makes a docstring
|
|
117
|
+
permanently unflaggable by SARJ050 — 126 first-party functions carry one and
|
|
118
|
+
SARJ050 flags none of them. The remedy deletes the section and keeps the
|
|
119
|
+
summary, which was checked against the shipped strict config: ruff's D417 does
|
|
120
|
+
not fire on a docstring with no parameter section.
|
|
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.
|
|
128
|
+
|
|
87
129
|
### House conventions moved out of consumer repos (0.21.0)
|
|
88
130
|
|
|
89
131
|
```yaml
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""Shared docstring analysis for the docstring-ceremony rules (SARJ050/084/085/086).
|
|
2
|
+
|
|
3
|
+
Four rules ask overlapping questions about a docstring — "does this text say
|
|
4
|
+
anything the signature does not?", "which decorators make this docstring an
|
|
5
|
+
artefact someone else reads?", "where does the `Args:` block start?" — and the
|
|
6
|
+
answers have to be identical across all four or the family contradicts itself.
|
|
7
|
+
SARJ050 owned all of this privately until SARJ084-086 needed the same
|
|
8
|
+
judgements; the definitions moved here unchanged rather than being copied.
|
|
9
|
+
|
|
10
|
+
**`restates` is a DELETION test, not a value test.** It answers only "every
|
|
11
|
+
content word of this text already appears in that identifier set". A False
|
|
12
|
+
result means the text carries a word the signature does not — that is all. It
|
|
13
|
+
must never be read as "this docstring is worthless"; the guards in each rule,
|
|
14
|
+
plus `_comments.is_protected`, are what turn a restatement into a finding.
|
|
15
|
+
|
|
16
|
+
**Section parsing is Google-style only.** `Args:` / `Returns:` / `Raises:` on
|
|
17
|
+
their own line. NumPy style (`Parameters` followed by a `-----` underline) is
|
|
18
|
+
deliberately not parsed: across 2,440 reviewable first-party files the corpus
|
|
19
|
+
holds **2** NumPy docstrings, which is far too little evidence to tune a second
|
|
20
|
+
parser against, and a half-recognised section is worse than an unrecognised one
|
|
21
|
+
— it would let a rule read a `Parameters` heading as prose and judge the block
|
|
22
|
+
on it.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import ast
|
|
28
|
+
import re
|
|
29
|
+
from typing import TYPE_CHECKING
|
|
30
|
+
|
|
31
|
+
from sarj_python_lint.rules._comments import split_identifier, stem
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from collections.abc import Iterable
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Docstring filler that says nothing about *which* thing is being described.
|
|
39
|
+
# `not` / `no` / `none` / `never` are deliberately ABSENT: a docstring that
|
|
40
|
+
# negates the obvious reading of a name is the most useful kind there is.
|
|
41
|
+
STOPWORDS = frozenset(
|
|
42
|
+
{
|
|
43
|
+
"a",
|
|
44
|
+
"all",
|
|
45
|
+
"an",
|
|
46
|
+
"and",
|
|
47
|
+
"are",
|
|
48
|
+
"as",
|
|
49
|
+
"at",
|
|
50
|
+
"based",
|
|
51
|
+
"be",
|
|
52
|
+
"been",
|
|
53
|
+
"being",
|
|
54
|
+
"by",
|
|
55
|
+
"class",
|
|
56
|
+
"current",
|
|
57
|
+
"do",
|
|
58
|
+
"does",
|
|
59
|
+
"false",
|
|
60
|
+
"for",
|
|
61
|
+
"from",
|
|
62
|
+
"function",
|
|
63
|
+
"get",
|
|
64
|
+
"gets",
|
|
65
|
+
"given",
|
|
66
|
+
"helper",
|
|
67
|
+
"if",
|
|
68
|
+
"in",
|
|
69
|
+
"instance",
|
|
70
|
+
"instances",
|
|
71
|
+
"into",
|
|
72
|
+
"is",
|
|
73
|
+
"it",
|
|
74
|
+
"its",
|
|
75
|
+
"method",
|
|
76
|
+
"new",
|
|
77
|
+
"object",
|
|
78
|
+
"objects",
|
|
79
|
+
"of",
|
|
80
|
+
"on",
|
|
81
|
+
"or",
|
|
82
|
+
"provided",
|
|
83
|
+
"return",
|
|
84
|
+
"returned",
|
|
85
|
+
"returns",
|
|
86
|
+
"s",
|
|
87
|
+
"set",
|
|
88
|
+
"sets",
|
|
89
|
+
"should",
|
|
90
|
+
"specified",
|
|
91
|
+
"that",
|
|
92
|
+
"the",
|
|
93
|
+
"these",
|
|
94
|
+
"this",
|
|
95
|
+
"those",
|
|
96
|
+
"to",
|
|
97
|
+
"true",
|
|
98
|
+
"using",
|
|
99
|
+
"value",
|
|
100
|
+
"values",
|
|
101
|
+
"was",
|
|
102
|
+
"when",
|
|
103
|
+
"whether",
|
|
104
|
+
"which",
|
|
105
|
+
"will",
|
|
106
|
+
"with",
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
WORD_RE = re.compile(r"[A-Za-z][A-Za-z0-9']*")
|
|
111
|
+
|
|
112
|
+
_IDENTIFIER_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
113
|
+
|
|
114
|
+
# Content the signature cannot carry, so the docstring is earning its place.
|
|
115
|
+
VALUE_MARKER_RE = re.compile(
|
|
116
|
+
r"https?://|\bRFC\s?\d|:raises|\bRaises:|>>>|\bExamples?:|^\s*\.\. |"
|
|
117
|
+
r"\b(?:ms|msec|milliseconds?|seconds?|secs?|minutes?|hours?|days?|bytes?|kb|mb|gb|hz|khz|"
|
|
118
|
+
r"utc|iso.?8601|e\.?164|base64|utf-?8|px|dbfs?|db)\b|%",
|
|
119
|
+
re.IGNORECASE | re.MULTILINE,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Decorators whose docstring is consumed by something other than a reader, so
|
|
123
|
+
# deleting it changes an artefact rather than tidying a file:
|
|
124
|
+
# - `function_tool` / `tool` hand it to a language model as the tool
|
|
125
|
+
# description, which is what the agent reasons over;
|
|
126
|
+
# - click and typer hand it to the terminal as `--help`;
|
|
127
|
+
# - FastAPI / Starlette / Flask routing decorators hand it to the OpenAPI
|
|
128
|
+
# schema as the operation description. That last one was found by the corpus
|
|
129
|
+
# sweep rather than predicted: a `@router.post(...)` handler's one-line
|
|
130
|
+
# docstring is the text an API consumer reads in the generated schema.
|
|
131
|
+
PROMPT_DECORATOR_MARKERS = frozenset(
|
|
132
|
+
{
|
|
133
|
+
"agent",
|
|
134
|
+
"api_route",
|
|
135
|
+
"app",
|
|
136
|
+
"blueprint",
|
|
137
|
+
"cli",
|
|
138
|
+
"click",
|
|
139
|
+
"command",
|
|
140
|
+
"delete",
|
|
141
|
+
"function_tool",
|
|
142
|
+
"get",
|
|
143
|
+
"group",
|
|
144
|
+
"mcp",
|
|
145
|
+
"option",
|
|
146
|
+
"patch",
|
|
147
|
+
"post",
|
|
148
|
+
"put",
|
|
149
|
+
"route",
|
|
150
|
+
"router",
|
|
151
|
+
"server",
|
|
152
|
+
"tool",
|
|
153
|
+
"tools",
|
|
154
|
+
"typer",
|
|
155
|
+
"websocket",
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Google-style section headers, each alone on its line. `Args`/`Returns` are the
|
|
160
|
+
# two the ceremony rules act on; the rest are listed so a rule can tell "this
|
|
161
|
+
# docstring has an `Examples:` block" from "this docstring has prose containing
|
|
162
|
+
# the word examples".
|
|
163
|
+
_SECTION_RE = re.compile(
|
|
164
|
+
r"^[ \t]*(?P<name>Args|Arguments|Parameters|Params|Keyword Args|Keyword Arguments|"
|
|
165
|
+
r"Returns|Return|Yields|Yield|Raises|Attributes|Example|Examples|Note|Notes|"
|
|
166
|
+
r"Warning|Warnings|Warns|See Also|References|Todo|Other Parameters|Methods)\s*:[ \t]*$",
|
|
167
|
+
re.MULTILINE,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# One `Args:` entry: `name (type): description`, with the type parenthesis
|
|
171
|
+
# optional. The leading indent is required — an unindented `name:` line is
|
|
172
|
+
# ordinary prose with a colon in it, not a parameter entry.
|
|
173
|
+
_ARG_ENTRY_RE = re.compile(r"^[ \t]+(?P<name>\*{0,2}[A-Za-z_]\w*)[ \t]*(?:\((?P<type>[^)]*)\))?[ \t]*:(?P<desc>.*)$")
|
|
174
|
+
|
|
175
|
+
ARG_SECTIONS = ("Args", "Arguments", "Parameters", "Params", "Keyword Args", "Keyword Arguments")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def sections(docstring: str) -> dict[str, str]:
|
|
179
|
+
"""Split a Google-style docstring into `{"summary": ..., "<Section>": ...}`.
|
|
180
|
+
|
|
181
|
+
A docstring with no recognised header is all summary. Two blocks under the
|
|
182
|
+
same header (which a hand-edited docstring does produce) concatenate.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
The summary and every recognised section body, keyed by header name.
|
|
186
|
+
|
|
187
|
+
"""
|
|
188
|
+
marks = [(match.start(), match.end(), match.group("name")) for match in _SECTION_RE.finditer(docstring)]
|
|
189
|
+
if not marks:
|
|
190
|
+
return {"summary": docstring}
|
|
191
|
+
out: dict[str, str] = {"summary": docstring[: marks[0][0]]}
|
|
192
|
+
for index, (_, header_end, name) in enumerate(marks):
|
|
193
|
+
body_end = marks[index + 1][0] if index + 1 < len(marks) else len(docstring)
|
|
194
|
+
out[name] = out.get(name, "") + docstring[header_end:body_end]
|
|
195
|
+
return out
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def arg_section(docstring: str) -> str | None:
|
|
199
|
+
"""Return the parameter-documentation block of `docstring`, if it has one.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
The section body, or None when the docstring documents no parameters.
|
|
203
|
+
|
|
204
|
+
"""
|
|
205
|
+
found = sections(docstring)
|
|
206
|
+
for name in ARG_SECTIONS:
|
|
207
|
+
if name in found:
|
|
208
|
+
return found[name]
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def arg_entries(block: str) -> list[tuple[str, str, str]]:
|
|
213
|
+
"""Parse an `Args:` block into `(name, type, description)` triples.
|
|
214
|
+
|
|
215
|
+
A line that is not an entry but follows one is that entry's wrapped
|
|
216
|
+
description. Folding those in is load-bearing rather than cosmetic: without
|
|
217
|
+
it the continuation row vanishes, and an entry whose informative half sits
|
|
218
|
+
on the second line reads as a bare restatement.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
One triple per documented parameter, in source order.
|
|
222
|
+
|
|
223
|
+
"""
|
|
224
|
+
entries: list[list[str]] = []
|
|
225
|
+
for raw in block.splitlines():
|
|
226
|
+
match = _ARG_ENTRY_RE.match(raw)
|
|
227
|
+
if match is not None:
|
|
228
|
+
entries.append([match.group("name"), match.group("type") or "", match.group("desc").strip()])
|
|
229
|
+
elif entries and raw.strip():
|
|
230
|
+
entries[-1][2] += " " + raw.strip()
|
|
231
|
+
return [(name, type_, desc) for name, type_, desc in entries]
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def identifier_stems(text: str) -> set[str]:
|
|
235
|
+
"""Collect the stemmed word parts of every identifier in `text`.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
The stems, lowercased.
|
|
239
|
+
|
|
240
|
+
"""
|
|
241
|
+
return {stem(part) for match in _IDENTIFIER_RE.finditer(text) for part in split_identifier(match.group(0))}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def decorator_markers(node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef) -> set[str]:
|
|
245
|
+
"""Collect the lowercase word parts of every decorator on `node`.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
The decorator name parts, for matching against `PROMPT_DECORATOR_MARKERS`.
|
|
249
|
+
|
|
250
|
+
"""
|
|
251
|
+
markers: set[str] = set()
|
|
252
|
+
for decorator in node.decorator_list:
|
|
253
|
+
target = decorator.func if isinstance(decorator, ast.Call) else decorator
|
|
254
|
+
try:
|
|
255
|
+
markers.update(part.lower() for part in re.split(r"\W+", ast.unparse(target)) if part)
|
|
256
|
+
except AttributeError, ValueError: # pragma: no cover — unparse is total for these nodes
|
|
257
|
+
continue
|
|
258
|
+
return markers
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def annotation_tokens(annotation: ast.expr | None) -> list[str]:
|
|
262
|
+
"""Split an annotation's rendered source into lowercase word parts.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
The word parts, or an empty list when there is no annotation.
|
|
266
|
+
|
|
267
|
+
"""
|
|
268
|
+
if annotation is None:
|
|
269
|
+
return []
|
|
270
|
+
try:
|
|
271
|
+
rendered = ast.unparse(annotation)
|
|
272
|
+
except AttributeError, ValueError: # pragma: no cover
|
|
273
|
+
return []
|
|
274
|
+
return [part for token in re.split(r"\W+", rendered) if token for part in split_identifier(token)]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def signature_stems(node: ast.FunctionDef | ast.AsyncFunctionDef, class_name: str | None) -> set[str]:
|
|
278
|
+
"""Collect every stem a reader can read off the signature.
|
|
279
|
+
|
|
280
|
+
The function name, the owning class name, every parameter name that is not
|
|
281
|
+
`self`/`cls`, and every annotation — parameter and return.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
The stems the signature already carries.
|
|
285
|
+
|
|
286
|
+
"""
|
|
287
|
+
tokens = list(split_identifier(node.name))
|
|
288
|
+
if class_name is not None:
|
|
289
|
+
tokens.extend(split_identifier(class_name))
|
|
290
|
+
args = node.args
|
|
291
|
+
for arg in [*args.posonlyargs, *args.args, *args.kwonlyargs, args.vararg, args.kwarg]:
|
|
292
|
+
if arg is None:
|
|
293
|
+
continue
|
|
294
|
+
if arg.arg not in {"self", "cls"}:
|
|
295
|
+
tokens.extend(split_identifier(arg.arg))
|
|
296
|
+
tokens.extend(annotation_tokens(arg.annotation))
|
|
297
|
+
tokens.extend(annotation_tokens(node.returns))
|
|
298
|
+
return {stem(token) for token in tokens}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def restates(text: str, known: Iterable[str]) -> bool:
|
|
302
|
+
"""Report whether every content word of `text` is already in `known`.
|
|
303
|
+
|
|
304
|
+
A text with no content words at all (pure stopwords, or no words) returns
|
|
305
|
+
False: "says nothing" and "says only what the code says" are different
|
|
306
|
+
findings, and only the caller knows which one it wants.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
True when `text` adds no word the identifier set does not carry.
|
|
310
|
+
|
|
311
|
+
"""
|
|
312
|
+
known_stems = set(known)
|
|
313
|
+
words = [match.group(0).lower() for match in WORD_RE.finditer(text)]
|
|
314
|
+
content = [word for word in words if word not in STOPWORDS]
|
|
315
|
+
if not content:
|
|
316
|
+
return False
|
|
317
|
+
return all(stem(word) in known_stems for word in content)
|
|
@@ -3,7 +3,13 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from sarj_python_lint.rules.conditional_assertion_in_test import ConditionalAssertionInTest
|
|
6
|
+
from sarj_python_lint.rules.docstring_args_restate_signature import (
|
|
7
|
+
DocstringArgsRestateSignature,
|
|
8
|
+
)
|
|
6
9
|
from sarj_python_lint.rules.duplicate_test_body import DuplicateTestBody
|
|
10
|
+
from sarj_python_lint.rules.duplicated_override_docstring import (
|
|
11
|
+
DuplicatedOverrideDocstring,
|
|
12
|
+
)
|
|
7
13
|
from sarj_python_lint.rules.fixture_returns_bare_tuple import FixtureReturnsBareTuple
|
|
8
14
|
from sarj_python_lint.rules.inefficient_string_concat_in_loop import (
|
|
9
15
|
InefficientStringConcatInLoop,
|
|
@@ -84,6 +90,7 @@ from sarj_python_lint.rules.prefer_walrus_comprehension_filter import (
|
|
|
84
90
|
from sarj_python_lint.rules.prefer_walrus_regex_match import PreferWalrusRegexMatch
|
|
85
91
|
from sarj_python_lint.rules.prefer_walrus_stream_loop import PreferWalrusStreamLoop
|
|
86
92
|
from sarj_python_lint.rules.pydantic_at_boundaries import PydanticAtBoundaries
|
|
93
|
+
from sarj_python_lint.rules.redundant_class_docstring import RedundantClassDocstring
|
|
87
94
|
from sarj_python_lint.rules.redundant_docstring import RedundantDocstring
|
|
88
95
|
from sarj_python_lint.rules.require_port_for_service import RequirePortForService
|
|
89
96
|
from sarj_python_lint.rules.single_public_export import SinglePublicExport
|
|
@@ -176,6 +183,9 @@ REGISTRY: dict[str, type[Rule]] = {
|
|
|
176
183
|
PreferWalrusComprehensionFilter.id: PreferWalrusComprehensionFilter,
|
|
177
184
|
PreferWalrusStreamLoop.id: PreferWalrusStreamLoop,
|
|
178
185
|
PreferSelfTypeAnnotation.id: PreferSelfTypeAnnotation,
|
|
186
|
+
DuplicatedOverrideDocstring.id: DuplicatedOverrideDocstring,
|
|
187
|
+
RedundantClassDocstring.id: RedundantClassDocstring,
|
|
188
|
+
DocstringArgsRestateSignature.id: DocstringArgsRestateSignature,
|
|
179
189
|
}
|
|
180
190
|
|
|
181
191
|
__all__ = ["REGISTRY"]
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""SARJ086: an `Args:` block that only re-spells the parameter list.
|
|
2
|
+
|
|
3
|
+
def delete(self, key: str) -> None:
|
|
4
|
+
\"\"\"Drop the entry, and the tombstone the compactor would have read.
|
|
5
|
+
|
|
6
|
+
Args: <- everything below is ceremony
|
|
7
|
+
key: The key of the value to delete
|
|
8
|
+
\"\"\"
|
|
9
|
+
|
|
10
|
+
The summary earns its place — "the tombstone the compactor would have read" is
|
|
11
|
+
not readable off the signature. The `Args:` block does not: every content word
|
|
12
|
+
of every entry is already in the parameter's own name, its annotation, or the
|
|
13
|
+
function name. It is a table of contents for a list of one. (That entry is real:
|
|
14
|
+
`celery/celery/backends/azureblockblob.py:154`.)
|
|
15
|
+
|
|
16
|
+
**The fix is to delete the `Args:` section only**, leaving the summary. That is
|
|
17
|
+
safe, and it was checked against the shipped strict config rather than assumed:
|
|
18
|
+
ruff's D417 (`undocumented-param`) does **not** fire on a Google-style docstring
|
|
19
|
+
with no parameter section at all, so removing the block does not trade one
|
|
20
|
+
finding for another. It is also the whole reason this rule exists and the
|
|
21
|
+
sibling `Returns:` shape does not — deleting a `Returns:` section makes DOC201
|
|
22
|
+
fire, so the only compliant remedy there is deleting a docstring whose summary
|
|
23
|
+
may be the valuable part.
|
|
24
|
+
|
|
25
|
+
**Why SARJ050 cannot reach this.** SARJ050 tests every content word of the
|
|
26
|
+
docstring against the signature stems. The literal header word "args" is a
|
|
27
|
+
content word and no signature contains it, so the mere presence of an `Args:`
|
|
28
|
+
block makes a docstring permanently unflaggable by that rule, whatever the block
|
|
29
|
+
says. Across the first-party corpus **126** functions carry a parsed `Args:`
|
|
30
|
+
block; SARJ050 flags **0** of them.
|
|
31
|
+
|
|
32
|
+
**Never flagged**
|
|
33
|
+
|
|
34
|
+
- **One informative entry protects the whole block.** The test is over every
|
|
35
|
+
entry: a block where three entries restate and the fourth carries a default, a
|
|
36
|
+
unit, an example value or a constraint stays whole. Splitting a parameter
|
|
37
|
+
table is worse than leaving it. Relaxing this to "any entry restates" raises
|
|
38
|
+
the first-party count from 12 to 16 and immediately admits entries documenting
|
|
39
|
+
defaults.
|
|
40
|
+
- **An entry with no description at all** — the bare `name (type):` stub. Every
|
|
41
|
+
one of the 8 first-party instances came from an OpenAPI client generator whose
|
|
42
|
+
output carries no generated-code marker, so a content-only check cannot see
|
|
43
|
+
it; judging a machine-emitted stub tells the author to edit a file that will
|
|
44
|
+
be regenerated. Dropping them is what takes the raw 20 findings to 12.
|
|
45
|
+
- **An empty block, or one no entry parses out of.** Nothing to judge.
|
|
46
|
+
- **Prompt / CLI / route decorators.** For an agent tool the `Args:` block is
|
|
47
|
+
part of the description shipped to the model; for click/typer it is the
|
|
48
|
+
argument help text — the same hard exemption SARJ050 makes.
|
|
49
|
+
- **The protected class and the value markers**, evaluated over the block, so a
|
|
50
|
+
parameter documented with a unit, a status code, an RFC, a ticket or a causal
|
|
51
|
+
clause keeps its whole table.
|
|
52
|
+
- **NumPy-style parameter blocks** (`Parameters` under a `-----` underline).
|
|
53
|
+
`_docstrings` parses Google style only; the first-party corpus holds 2 NumPy
|
|
54
|
+
docstrings in total, which is not enough evidence to tune a second parser.
|
|
55
|
+
|
|
56
|
+
**What counts as "already in the signature".** The function's own name and its
|
|
57
|
+
owning class contribute stems, not just the parameter's. An entry reading
|
|
58
|
+
`token: JWT access token to verify` on a `JwtService.verify_access_token(token: str)`
|
|
59
|
+
shape is a restatement, because "JWT" is on the class the caller types. That is also the
|
|
60
|
+
loosest the test gets: of the 12 first-party findings, exactly one turned on a
|
|
61
|
+
word supplied by the function name rather than the parameter, and it was judged
|
|
62
|
+
borderline-true rather than false.
|
|
63
|
+
|
|
64
|
+
**Measured.** 20 raw findings across 2,440 reviewable first-party files, 8 of
|
|
65
|
+
them generator output that the empty-description guard removes, leaving **12**.
|
|
66
|
+
All 12 were read: **12 true positives, 0 false** (1 borderline, above). The
|
|
67
|
+
dominant shape is an ID parameter documented as its own name in title case.
|
|
68
|
+
|
|
69
|
+
Over 14 OSS repos the predicate finds **864** (langchain 257, mlflow 256,
|
|
70
|
+
dagster 137, litellm 107, prefect 87, celery 8, superset 8, airflow 4, and 0 in
|
|
71
|
+
django, fastapi, saleor, sentry-python, warehouse, zulip). 20 were sampled
|
|
72
|
+
across celery, superset and airflow and read: **20 true positives, 0 false**,
|
|
73
|
+
including `celery/celery/app/task.py:1030` ("sig (Signature): signature to
|
|
74
|
+
replace with."), `celery/celery/backends/cosmosdbsql.py:206`,
|
|
75
|
+
`superset/superset/utils/jinja_template_validator.py:38` ("template_str: The
|
|
76
|
+
template string to validate") and
|
|
77
|
+
`airflow/providers/openlineage/src/airflow/providers/openlineage/utils/spark.py:131`
|
|
78
|
+
("properties: Spark properties.").
|
|
79
|
+
|
|
80
|
+
Suppress an intentional case with `# sarj-noqa: SARJ086 — <reason>`.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
from __future__ import annotations
|
|
84
|
+
|
|
85
|
+
import ast
|
|
86
|
+
from typing import TYPE_CHECKING, override
|
|
87
|
+
|
|
88
|
+
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
89
|
+
from sarj_python_lint.rules._ast_index import children
|
|
90
|
+
from sarj_python_lint.rules._comments import is_protected
|
|
91
|
+
from sarj_python_lint.rules._docstrings import (
|
|
92
|
+
PROMPT_DECORATOR_MARKERS,
|
|
93
|
+
VALUE_MARKER_RE,
|
|
94
|
+
arg_entries,
|
|
95
|
+
arg_section,
|
|
96
|
+
decorator_markers,
|
|
97
|
+
identifier_stems,
|
|
98
|
+
restates,
|
|
99
|
+
signature_stems,
|
|
100
|
+
)
|
|
101
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if TYPE_CHECKING:
|
|
105
|
+
from pathlib import Path
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class DocstringArgsRestateSignature(Rule):
|
|
109
|
+
"""An `Args:` block whose every entry only re-spells its own parameter."""
|
|
110
|
+
|
|
111
|
+
id: str = "docstring-args-restate-signature"
|
|
112
|
+
code: str = "SARJ086"
|
|
113
|
+
description: str = (
|
|
114
|
+
"`Args:` block adds nothing the signature does not already say — delete "
|
|
115
|
+
"the section and keep the summary."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
@override
|
|
119
|
+
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
120
|
+
if is_generated(path, source):
|
|
121
|
+
return []
|
|
122
|
+
tree = parse_or_none(path, source)
|
|
123
|
+
if tree is None:
|
|
124
|
+
return []
|
|
125
|
+
diags: list[Diagnostic] = []
|
|
126
|
+
self._walk(tree, None, path, diags)
|
|
127
|
+
return sorted(diags, key=lambda d: d.line)
|
|
128
|
+
|
|
129
|
+
def _walk(self, node: ast.AST, class_name: str | None, path: Path, diags: list[Diagnostic]) -> None:
|
|
130
|
+
for child in children(node):
|
|
131
|
+
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
132
|
+
self._check_function(child, class_name, path, diags)
|
|
133
|
+
self._walk(child, class_name, path, diags)
|
|
134
|
+
elif isinstance(child, ast.ClassDef):
|
|
135
|
+
self._walk(child, child.name, path, diags)
|
|
136
|
+
else:
|
|
137
|
+
self._walk(child, class_name, path, diags)
|
|
138
|
+
|
|
139
|
+
def _check_function(
|
|
140
|
+
self,
|
|
141
|
+
node: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
142
|
+
class_name: str | None,
|
|
143
|
+
path: Path,
|
|
144
|
+
diags: list[Diagnostic],
|
|
145
|
+
) -> None:
|
|
146
|
+
docstring = ast.get_docstring(node, clean=True)
|
|
147
|
+
if not docstring:
|
|
148
|
+
return
|
|
149
|
+
block = arg_section(docstring)
|
|
150
|
+
if block is None or VALUE_MARKER_RE.search(block) or is_protected(block):
|
|
151
|
+
return
|
|
152
|
+
if decorator_markers(node) & PROMPT_DECORATOR_MARKERS:
|
|
153
|
+
return
|
|
154
|
+
entries = arg_entries(block)
|
|
155
|
+
if not entries:
|
|
156
|
+
return
|
|
157
|
+
known = signature_stems(node, class_name)
|
|
158
|
+
for name, annotation, description in entries:
|
|
159
|
+
if not description:
|
|
160
|
+
return # a machine-emitted `name (type):` stub — see the module docstring
|
|
161
|
+
if not restates(description, known | identifier_stems(name) | identifier_stems(annotation)):
|
|
162
|
+
return
|
|
163
|
+
expr = node.body[0]
|
|
164
|
+
diags.append(
|
|
165
|
+
Diagnostic(
|
|
166
|
+
path=path,
|
|
167
|
+
line=expr.lineno,
|
|
168
|
+
col=expr.col_offset + 1,
|
|
169
|
+
code=self.code,
|
|
170
|
+
message=self.description,
|
|
171
|
+
)
|
|
172
|
+
)
|