sarj-python-lint 0.41.0__tar.gz → 0.43.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.41.0 → sarj_python_lint-0.43.0}/PKG-INFO +3 -3
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/README.md +2 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/pyproject.toml +1 -1
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/__main__.py +7 -4
- sarj_python_lint-0.43.0/src/sarj_python_lint/_secret_names.py +109 -0
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rule_base.py +14 -28
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/_docstrings.py +1 -1
- sarj_python_lint-0.43.0/src/sarj_python_lint/rules/_prose_budget.py +151 -0
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/_registry.py +8 -0
- sarj_python_lint-0.43.0/src/sarj_python_lint/rules/_sql.py +78 -0
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/conditional_assertion_in_test.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/duplicate_test_body.py +18 -5
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/duplicated_override_docstring.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/inefficient_string_concat_in_loop.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/interaction_only_test.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +4 -39
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/kwonly_same_type_params.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/mock_without_spec.py +22 -3
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +2 -11
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_fat_try_blocks.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +3 -16
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_fstring_in_log.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +2 -10
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -2
- sarj_python_lint-0.43.0/src/sarj_python_lint/rules/no_long_comment.py +31 -0
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_offset_pagination.py +2 -11
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +2 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_raw_sql_in_tests.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_restated_comment.py +10 -86
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_select_star.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_sequential_await.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_sleep_in_test_body.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -2
- sarj_python_lint-0.43.0/src/sarj_python_lint/rules/no_typed_doc_sections.py +30 -0
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/no_unreachable_after_terminal.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/parametrize_case_needs_id.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/prefer_match_pattern_destructuring.py +2 -12
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -2
- sarj_python_lint-0.43.0/src/sarj_python_lint/rules/prefer_nominal_id_types.py +219 -0
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/prefer_real_store_in_tests.py +1 -3
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +3 -5
- sarj_python_lint-0.43.0/src/sarj_python_lint/rules/prefer_single_sentence_comment.py +31 -0
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/redundant_docstring.py +4 -19
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/require_port_for_service.py +24 -11
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/single_public_export.py +7 -5
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/sleep_with_computed_arg_in_test.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/stepdown.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +5 -11
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/tautological_mock_assertion.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/test_loops_over_literal_cases.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/xfail_requires_strict.py +0 -2
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/zero_assertion_test.py +9 -96
- sarj_python_lint-0.41.0/src/sarj_python_lint/_secret_names.py +0 -182
- sarj_python_lint-0.41.0/src/sarj_python_lint/rules/_sql.py +0 -121
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/.gitignore +0 -0
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/_suppression_comments.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.43.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
|
|
@@ -157,7 +157,7 @@ the shipped config the section goes and the summary stays.
|
|
|
157
157
|
read against source gave **~2%** false positives, the whole of which was one
|
|
158
158
|
family (`Returns: A new X` — whether the value is a copy is the one thing
|
|
159
159
|
`-> Self` cannot say) now guarded. Three findings on this repo's own source, all
|
|
160
|
-
true, all deleted.
|
|
160
|
+
true, all deleted. The guarded copy-return case is covered by the paired rule tests.
|
|
161
161
|
|
|
162
162
|
### Test ceremony, and the census it was chosen from (0.38.0)
|
|
163
163
|
|
|
@@ -169,7 +169,7 @@ true, all deleted. Measurements: [docs/rules/SARJ087.md](../../docs/rules/SARJ08
|
|
|
169
169
|
Every comment GROUP in 19 repositories / 45,900 Python files was collected with
|
|
170
170
|
its adjacent code and classified: **451,482 groups, 1,293,022 lines**, of which
|
|
171
171
|
the seven comment/docstring rules that predate this release reached **4.9%**.
|
|
172
|
-
The
|
|
172
|
+
The shipped predicate and its boundaries are recorded in the paired rule tests.
|
|
173
173
|
|
|
174
174
|
The largest precisely-detectable class left in it is the **test docstring**:
|
|
175
175
|
52,894 of them, 10.1% of every comment group, and SARJ050 reached 4.7%. It
|
|
@@ -139,7 +139,7 @@ the shipped config the section goes and the summary stays.
|
|
|
139
139
|
read against source gave **~2%** false positives, the whole of which was one
|
|
140
140
|
family (`Returns: A new X` — whether the value is a copy is the one thing
|
|
141
141
|
`-> Self` cannot say) now guarded. Three findings on this repo's own source, all
|
|
142
|
-
true, all deleted.
|
|
142
|
+
true, all deleted. The guarded copy-return case is covered by the paired rule tests.
|
|
143
143
|
|
|
144
144
|
### Test ceremony, and the census it was chosen from (0.38.0)
|
|
145
145
|
|
|
@@ -151,7 +151,7 @@ true, all deleted. Measurements: [docs/rules/SARJ087.md](../../docs/rules/SARJ08
|
|
|
151
151
|
Every comment GROUP in 19 repositories / 45,900 Python files was collected with
|
|
152
152
|
its adjacent code and classified: **451,482 groups, 1,293,022 lines**, of which
|
|
153
153
|
the seven comment/docstring rules that predate this release reached **4.9%**.
|
|
154
|
-
The
|
|
154
|
+
The shipped predicate and its boundaries are recorded in the paired rule tests.
|
|
155
155
|
|
|
156
156
|
The largest precisely-detectable class left in it is the **test docstring**:
|
|
157
157
|
52,894 of them, 10.1% of every comment group, and SARJ050 reached 4.7%. It
|
|
@@ -9,7 +9,7 @@ from pathlib import Path
|
|
|
9
9
|
import sys
|
|
10
10
|
|
|
11
11
|
from sarj_python_lint import __version__
|
|
12
|
-
from sarj_python_lint.rule_base import Diagnostic, is_suppressed
|
|
12
|
+
from sarj_python_lint.rule_base import Diagnostic, Severity, is_suppressed
|
|
13
13
|
from sarj_python_lint.rules import REGISTRY
|
|
14
14
|
|
|
15
15
|
|
|
@@ -117,14 +117,14 @@ def _explain(wanted: str) -> int:
|
|
|
117
117
|
sys.stdout.write(
|
|
118
118
|
f"{cls.code} {cls.id}\n{summary[0] if summary else cls.description}\nexamples: {cls.examples_url()}\n"
|
|
119
119
|
)
|
|
120
|
-
if cls.has_evidence:
|
|
121
|
-
sys.stdout.write(f"evidence: {cls.evidence_url()}\n")
|
|
122
120
|
return 0
|
|
123
121
|
|
|
124
122
|
|
|
125
123
|
def _baseline_counts(diags: list[Diagnostic]) -> dict[str, dict[str, int]]:
|
|
126
124
|
counts: dict[str, dict[str, int]] = {}
|
|
127
125
|
for d in diags:
|
|
126
|
+
if d.severity is Severity.WARNING:
|
|
127
|
+
continue
|
|
128
128
|
counts.setdefault(str(d.path), {})
|
|
129
129
|
counts[str(d.path)][d.code] = counts[str(d.path)].get(d.code, 0) + 1
|
|
130
130
|
return counts
|
|
@@ -160,6 +160,9 @@ def _apply_baseline(diags: list[Diagnostic], baseline: dict[str, dict[str, int]]
|
|
|
160
160
|
seen: Counter[tuple[str, str]] = Counter()
|
|
161
161
|
out: list[Diagnostic] = []
|
|
162
162
|
for d in diags:
|
|
163
|
+
if d.severity is Severity.WARNING:
|
|
164
|
+
out.append(d)
|
|
165
|
+
continue
|
|
163
166
|
key = (str(d.path), d.code)
|
|
164
167
|
seen[key] += 1
|
|
165
168
|
if seen[key] > baseline.get(key[0], {}).get(key[1], 0):
|
|
@@ -221,7 +224,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
221
224
|
diags = _apply_baseline(diags, _read_baseline(args.baseline))
|
|
222
225
|
for d in diags:
|
|
223
226
|
sys.stdout.write(d.format() + "\n")
|
|
224
|
-
return 1 if diags else 0
|
|
227
|
+
return 1 if any(d.severity is Severity.ERROR for d in diags) else 0
|
|
225
228
|
|
|
226
229
|
|
|
227
230
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Recognize secret-bearing identifiers for SARJ011 and SARJ012.
|
|
2
|
+
|
|
3
|
+
Matching uses whole words and excludes metadata and boolean-state names.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from itertools import pairwise
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
SECRET_WORDS = frozenset(
|
|
13
|
+
{
|
|
14
|
+
"token",
|
|
15
|
+
"secret",
|
|
16
|
+
"password",
|
|
17
|
+
"passwd",
|
|
18
|
+
"jwt",
|
|
19
|
+
"secrets",
|
|
20
|
+
"passwords",
|
|
21
|
+
"credential",
|
|
22
|
+
"credentials",
|
|
23
|
+
"authorization",
|
|
24
|
+
"signature",
|
|
25
|
+
"hmac",
|
|
26
|
+
"digest",
|
|
27
|
+
"hash",
|
|
28
|
+
"apikey",
|
|
29
|
+
# Keep this vocabulary aligned with the TypeScript helper.
|
|
30
|
+
"bearer",
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_SECRET_WORDS = SECRET_WORDS
|
|
35
|
+
|
|
36
|
+
# A trailing word in this set makes the identifier metadata, not a credential.
|
|
37
|
+
_INNOCUOUS_WORDS = frozenset(
|
|
38
|
+
{
|
|
39
|
+
"count",
|
|
40
|
+
"counts",
|
|
41
|
+
"budget",
|
|
42
|
+
"limit",
|
|
43
|
+
"limits",
|
|
44
|
+
"id",
|
|
45
|
+
"ids",
|
|
46
|
+
"enabled",
|
|
47
|
+
"disabled",
|
|
48
|
+
"flag",
|
|
49
|
+
"flags",
|
|
50
|
+
"present",
|
|
51
|
+
"set",
|
|
52
|
+
"unset",
|
|
53
|
+
"configured",
|
|
54
|
+
"missing",
|
|
55
|
+
"required",
|
|
56
|
+
"valid",
|
|
57
|
+
"invalid",
|
|
58
|
+
"exists",
|
|
59
|
+
"type",
|
|
60
|
+
"types",
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# A leading predicate makes the identifier a boolean flag, not a credential.
|
|
65
|
+
_FLAG_PREFIXES = frozenset({"is", "has", "was", "are", "can", "should"})
|
|
66
|
+
|
|
67
|
+
# camelCase / PascalCase / ALLCAPS / digit run splitter, applied to each
|
|
68
|
+
# snake/kebab segment. `APIKey` -> ["API", "Key"], `authToken` -> ["auth", "Token"].
|
|
69
|
+
_CAMEL_RE = re.compile(r"[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|\d+")
|
|
70
|
+
_SEGMENT_RE = re.compile(r"[^A-Za-z0-9]+")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def identifier_tokens(identifier: str) -> list[str]:
|
|
74
|
+
"""Return lowercase whole segments and their camel-case words."""
|
|
75
|
+
tokens: list[str] = []
|
|
76
|
+
for segment in _SEGMENT_RE.split(identifier):
|
|
77
|
+
if not segment:
|
|
78
|
+
continue
|
|
79
|
+
tokens.append(segment.lower())
|
|
80
|
+
camel_parts: list[str] = _CAMEL_RE.findall(segment)
|
|
81
|
+
tokens.extend(part.lower() for part in camel_parts)
|
|
82
|
+
return tokens
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def leading_word(identifier: str) -> str | None:
|
|
86
|
+
"""Return the first camel- or delimiter-separated word, lowercased."""
|
|
87
|
+
for segment in _SEGMENT_RE.split(identifier):
|
|
88
|
+
if not segment:
|
|
89
|
+
continue
|
|
90
|
+
parts: list[str] = _CAMEL_RE.findall(segment)
|
|
91
|
+
return parts[0].lower() if parts else segment.lower()
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def is_secret_name(identifier: str) -> bool:
|
|
96
|
+
"""Report whether `identifier` names raw secret material (a credential, not metadata)."""
|
|
97
|
+
tokens = identifier_tokens(identifier)
|
|
98
|
+
if tokens and tokens[-1] in _INNOCUOUS_WORDS:
|
|
99
|
+
return False
|
|
100
|
+
if leading_word(identifier) in _FLAG_PREFIXES:
|
|
101
|
+
return False
|
|
102
|
+
if any(tok in SECRET_WORDS for tok in tokens):
|
|
103
|
+
return True
|
|
104
|
+
return _has_api_key(tokens)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _has_api_key(tokens: list[str]) -> bool:
|
|
108
|
+
"""Report whether `api` is immediately followed by `key` (the split form of `api_key`)."""
|
|
109
|
+
return any(a == "api" and b == "key" for a, b in pairwise(tokens))
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
6
|
import ast
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
+
from enum import StrEnum
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
import re
|
|
10
11
|
from typing import TYPE_CHECKING, Final
|
|
@@ -14,15 +15,9 @@ if TYPE_CHECKING:
|
|
|
14
15
|
from collections.abc import Sequence
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
#
|
|
18
|
-
# that point at them. Named once here because `Rule.examples_path` /
|
|
19
|
-
# `Rule.evidence_path` DERIVE every rule's links from these plus the rule's own
|
|
20
|
-
# module name and code — no rule module writes a URL by hand, so a rename cannot
|
|
21
|
-
# leave a dead link behind. `test_rule_meta.py` asserts every derived path
|
|
22
|
-
# resolves on disk.
|
|
18
|
+
# Each rule points directly to its executable examples.
|
|
23
19
|
REPO_BLOB: Final = "https://github.com/sarj-ai/standards/blob/main"
|
|
24
20
|
TESTS_DIR: Final = "packages/python/tests/rules"
|
|
25
|
-
EVIDENCE_DIR: Final = "docs/rules"
|
|
26
21
|
|
|
27
22
|
|
|
28
23
|
# Suppression syntax. Two forms supported:
|
|
@@ -58,6 +53,13 @@ def is_suppressed(source_lines: Sequence[str], line: int, code: str) -> bool:
|
|
|
58
53
|
return code.upper() in codes
|
|
59
54
|
|
|
60
55
|
|
|
56
|
+
class Severity(StrEnum):
|
|
57
|
+
"""Whether a diagnostic blocks the lint command."""
|
|
58
|
+
|
|
59
|
+
WARNING = "warning"
|
|
60
|
+
ERROR = "error"
|
|
61
|
+
|
|
62
|
+
|
|
61
63
|
@dataclass(frozen=True, slots=True)
|
|
62
64
|
class Diagnostic:
|
|
63
65
|
"""A single lint finding."""
|
|
@@ -67,10 +69,12 @@ class Diagnostic:
|
|
|
67
69
|
col: int
|
|
68
70
|
code: str
|
|
69
71
|
message: str
|
|
72
|
+
severity: Severity = Severity.ERROR
|
|
70
73
|
|
|
71
74
|
def format(self) -> str:
|
|
72
75
|
"""Render the finding ruff-compatibly as `path:line:col: CODE message`."""
|
|
73
|
-
|
|
76
|
+
label = "warning: " if self.severity is Severity.WARNING else ""
|
|
77
|
+
return f"{self.path}:{self.line}:{self.col}: {self.code} {label}{self.message}"
|
|
74
78
|
|
|
75
79
|
|
|
76
80
|
class Rule(ABC):
|
|
@@ -79,24 +83,14 @@ class Rule(ABC):
|
|
|
79
83
|
Subclasses set `id` (kebab-case) and `code` (e.g. SARJ001) as class
|
|
80
84
|
attributes and implement `check(path, source) -> list[Diagnostic]`.
|
|
81
85
|
|
|
82
|
-
A rule documents itself with a
|
|
83
|
-
|
|
84
|
-
are computed from `__module__` and `code`, never written by hand, and
|
|
85
|
-
`test_rule_meta.py` asserts each resolves to a file that exists. A rename
|
|
86
|
-
therefore fails the suite instead of leaving a dead link in a docstring.
|
|
86
|
+
A rule documents itself with a concise summary and a derived link to its
|
|
87
|
+
executable examples. The metadata tests require that paired test module.
|
|
87
88
|
"""
|
|
88
89
|
|
|
89
90
|
id: str
|
|
90
91
|
code: str
|
|
91
92
|
description: str
|
|
92
93
|
|
|
93
|
-
# True when `docs/rules/<code>.md` holds this rule's measured evidence — the
|
|
94
|
-
# corpus census, the threshold sweeps and the false-positive families each
|
|
95
|
-
# guard was built to stop. Declared rather than probed so the link still
|
|
96
|
-
# prints from an installed wheel, where `docs/` is not shipped;
|
|
97
|
-
# `test_evidence_flag_matches_the_filesystem` keeps the two honest.
|
|
98
|
-
has_evidence: bool = False
|
|
99
|
-
|
|
100
94
|
@abstractmethod
|
|
101
95
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
102
96
|
"""Inspect the given source. Return zero or more diagnostics."""
|
|
@@ -106,18 +100,10 @@ class Rule(ABC):
|
|
|
106
100
|
def examples_path(cls) -> str:
|
|
107
101
|
return f"{TESTS_DIR}/test_{cls.__module__.rpartition('.')[2]}.py"
|
|
108
102
|
|
|
109
|
-
@classmethod
|
|
110
|
-
def evidence_path(cls) -> str:
|
|
111
|
-
return f"{EVIDENCE_DIR}/{cls.code}.md"
|
|
112
|
-
|
|
113
103
|
@classmethod
|
|
114
104
|
def examples_url(cls) -> str:
|
|
115
105
|
return f"{REPO_BLOB}/{cls.examples_path()}"
|
|
116
106
|
|
|
117
|
-
@classmethod
|
|
118
|
-
def evidence_url(cls) -> str:
|
|
119
|
-
return f"{REPO_BLOB}/{cls.evidence_path()}"
|
|
120
|
-
|
|
121
107
|
|
|
122
108
|
_last_parse: tuple[str, str, ast.Module | None] | None = None
|
|
123
109
|
|
{sarj_python_lint-0.41.0 → sarj_python_lint-0.43.0}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
@@ -111,7 +111,7 @@ _BASE_STOPWORDS = frozenset(
|
|
|
111
111
|
# config", "the entire widget" — strip the adjective and the sentence means
|
|
112
112
|
# exactly what it meant. Kept as its own name because a single one of these
|
|
113
113
|
# was the commonest reason a pure restatement survived the whole family; see
|
|
114
|
-
#
|
|
114
|
+
# The paired SARJ088 tests cover this vocabulary. `main`, `new`, `same` and `copy`
|
|
115
115
|
# are NOT here: they name the thing or its identity, which is content.
|
|
116
116
|
FILLER_QUALIFIERS = frozenset(
|
|
117
117
|
{
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Shared extraction and sentence counting for SARJ090-092."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
import io
|
|
8
|
+
import re
|
|
9
|
+
import tokenize
|
|
10
|
+
from typing import TYPE_CHECKING, Final
|
|
11
|
+
|
|
12
|
+
from sarj_python_lint.rule_base import parse_or_none
|
|
13
|
+
from sarj_python_lint.rules._docstrings import PROMPT_DECORATOR_MARKERS, decorator_markers, sections
|
|
14
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_DIRECTIVE_RE: Final = re.compile(
|
|
22
|
+
r"^(?:!|noqa|sarj-noqa|type:|pragma|pyright|mypy|fmt:|isort|ruff|pylint|flake8|"
|
|
23
|
+
r"nosec|nosemgrep|todo|fixme|hack\b|xxx|coding[:=])",
|
|
24
|
+
re.IGNORECASE,
|
|
25
|
+
)
|
|
26
|
+
_LICENSE_RE: Final = re.compile(r"\b(?:copyright|spdx-license-identifier|licensed under)\b", re.IGNORECASE)
|
|
27
|
+
_BOUNDARY_RE: Final = re.compile(r"(?<=[.!?])[\"'`)\]]*\s+(?=[A-Z0-9`])")
|
|
28
|
+
_BULLET_RE: Final = re.compile(r"^\s*(?:[-*+] |\d+[.)] )")
|
|
29
|
+
_TYPED_SECTIONS: Final = frozenset(
|
|
30
|
+
{"Args", "Arguments", "Parameters", "Params", "Keyword Args", "Keyword Arguments", "Returns", "Return", "Yields", "Yield"}
|
|
31
|
+
)
|
|
32
|
+
_SCHEMA_BASES: Final = frozenset(
|
|
33
|
+
{"BaseModel", "BaseSettings", "RootModel", "TypedDict", "Enum", "IntEnum", "StrEnum"}
|
|
34
|
+
)
|
|
35
|
+
_SCHEMA_DECORATORS: Final = frozenset({"pydantic", "strawberry", "graphene", "msgspec"})
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class ProseGroup:
|
|
40
|
+
line: int
|
|
41
|
+
col: int
|
|
42
|
+
text: str
|
|
43
|
+
kind: str
|
|
44
|
+
typed_sections: frozenset[str] = frozenset()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def sentence_units(text: str) -> int:
|
|
48
|
+
"""Count prose sentences and unpunctuated list items deterministically."""
|
|
49
|
+
cleaned = re.sub(r"https?://\S+", "URL", text)
|
|
50
|
+
cleaned = re.sub(r"`[^`\n]+`", "CODE", cleaned)
|
|
51
|
+
cleaned = re.sub(r"\b\d+\.\d+\b", "NUMBER", cleaned)
|
|
52
|
+
cleaned = re.sub(r"\b(?:e\.g\.|i\.e\.|vs\.|etc\.)", "ABBREVIATION", cleaned, flags=re.IGNORECASE)
|
|
53
|
+
units = 0
|
|
54
|
+
prose: list[str] = []
|
|
55
|
+
for raw in cleaned.splitlines():
|
|
56
|
+
line = raw.strip().lstrip("*").strip()
|
|
57
|
+
if not line or re.fullmatch(r"[A-Za-z][A-Za-z ]+:", line):
|
|
58
|
+
continue
|
|
59
|
+
if _BULLET_RE.match(line):
|
|
60
|
+
units += 1
|
|
61
|
+
else:
|
|
62
|
+
prose.append(line)
|
|
63
|
+
paragraph = " ".join(prose).strip()
|
|
64
|
+
if paragraph:
|
|
65
|
+
units += len(_BOUNDARY_RE.split(paragraph))
|
|
66
|
+
return units
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def groups(path: Path, source: str) -> list[ProseGroup]:
|
|
70
|
+
"""Extract docstrings and contiguous own-line comment runs from one file."""
|
|
71
|
+
if is_generated(path, source):
|
|
72
|
+
return []
|
|
73
|
+
tree = parse_or_none(path, source)
|
|
74
|
+
if tree is None:
|
|
75
|
+
return []
|
|
76
|
+
out = _docstring_groups(tree)
|
|
77
|
+
lines = source.splitlines()
|
|
78
|
+
try:
|
|
79
|
+
comments = [token for token in tokenize.generate_tokens(io.StringIO(source).readline) if token.type == tokenize.COMMENT]
|
|
80
|
+
except tokenize.TokenError, IndentationError, SyntaxError:
|
|
81
|
+
return out
|
|
82
|
+
run: list[tokenize.TokenInfo] = []
|
|
83
|
+
for comment in comments:
|
|
84
|
+
body = comment.string.removeprefix("#").strip()
|
|
85
|
+
own_line = not lines[comment.start[0] - 1][: comment.start[1]].strip()
|
|
86
|
+
if not own_line or _DIRECTIVE_RE.match(body) or _LICENSE_RE.search(body):
|
|
87
|
+
if run:
|
|
88
|
+
out.append(_comment_run(run))
|
|
89
|
+
run = []
|
|
90
|
+
continue
|
|
91
|
+
if run and (comment.start[0] != run[-1].end[0] + 1 or comment.start[1] != run[-1].start[1]):
|
|
92
|
+
out.append(_comment_run(run))
|
|
93
|
+
run = []
|
|
94
|
+
run.append(comment)
|
|
95
|
+
if run:
|
|
96
|
+
out.append(_comment_run(run))
|
|
97
|
+
return out
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _comment_run(run: list[tokenize.TokenInfo]) -> ProseGroup:
|
|
101
|
+
return ProseGroup(
|
|
102
|
+
line=run[0].start[0],
|
|
103
|
+
col=run[0].start[1] + 1,
|
|
104
|
+
text="\n".join(token.string.removeprefix("#").strip() for token in run),
|
|
105
|
+
kind="comment",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _docstring_groups(tree: ast.Module) -> list[ProseGroup]:
|
|
110
|
+
out: list[ProseGroup] = []
|
|
111
|
+
for node in ast.walk(tree):
|
|
112
|
+
if not isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)) or not node.body:
|
|
113
|
+
continue
|
|
114
|
+
first = node.body[0]
|
|
115
|
+
if not (
|
|
116
|
+
isinstance(first, ast.Expr)
|
|
117
|
+
and isinstance(first.value, ast.Constant)
|
|
118
|
+
and isinstance(first.value.value, str)
|
|
119
|
+
):
|
|
120
|
+
continue
|
|
121
|
+
if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)) and (
|
|
122
|
+
decorator_markers(node) & PROMPT_DECORATOR_MARKERS
|
|
123
|
+
):
|
|
124
|
+
continue
|
|
125
|
+
if isinstance(node, ast.ClassDef) and _is_schema_class(node):
|
|
126
|
+
continue
|
|
127
|
+
doc = first.value.value
|
|
128
|
+
found = frozenset(
|
|
129
|
+
name
|
|
130
|
+
for name in sections(doc)
|
|
131
|
+
if name in _TYPED_SECTIONS and isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and _fully_typed(node)
|
|
132
|
+
)
|
|
133
|
+
out.append(ProseGroup(first.lineno, first.col_offset + 1, doc, "docstring", found))
|
|
134
|
+
return out
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _is_schema_class(node: ast.ClassDef) -> bool:
|
|
138
|
+
bases = {
|
|
139
|
+
target.attr if isinstance(target, ast.Attribute) else target.id
|
|
140
|
+
for base in node.bases
|
|
141
|
+
if isinstance((target := base.value if isinstance(base, ast.Subscript) else base), (ast.Attribute, ast.Name))
|
|
142
|
+
}
|
|
143
|
+
return bool(bases & _SCHEMA_BASES or decorator_markers(node) & _SCHEMA_DECORATORS)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _fully_typed(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
147
|
+
args = node.args
|
|
148
|
+
parameters = (*args.posonlyargs, *args.args, *args.kwonlyargs)
|
|
149
|
+
return node.returns is not None and all(arg.arg in {"self", "cls"} or arg.annotation is not None for arg in parameters) and (
|
|
150
|
+
args.vararg is None or args.vararg.annotation is not None
|
|
151
|
+
) and (args.kwarg is None or args.kwarg.annotation is not None)
|
|
@@ -37,6 +37,7 @@ from sarj_python_lint.rules.no_first_party_private_import import (
|
|
|
37
37
|
from sarj_python_lint.rules.no_fstring_in_log import NoFstringInLog
|
|
38
38
|
from sarj_python_lint.rules.no_gen_random_uuid_in_sql import NoGenRandomUuidInSql
|
|
39
39
|
from sarj_python_lint.rules.no_isinstance_union_chain import NoIsinstanceUnionChain
|
|
40
|
+
from sarj_python_lint.rules.no_long_comment import NoLongComment
|
|
40
41
|
from sarj_python_lint.rules.no_offset_pagination import NoOffsetPagination
|
|
41
42
|
from sarj_python_lint.rules.no_optional_tenant_predicate import (
|
|
42
43
|
NoOptionalTenantPredicate,
|
|
@@ -52,6 +53,7 @@ from sarj_python_lint.rules.no_sequential_await import NoSequentialAwait
|
|
|
52
53
|
from sarj_python_lint.rules.no_sleep_in_test_body import NoSleepInTestBody
|
|
53
54
|
from sarj_python_lint.rules.no_stdlib_logging import NoStdlibLogging
|
|
54
55
|
from sarj_python_lint.rules.no_tautological_expect import NoTautologicalExpect
|
|
56
|
+
from sarj_python_lint.rules.no_typed_doc_sections import NoTypedDocSections
|
|
55
57
|
from sarj_python_lint.rules.no_unreachable_after_terminal import (
|
|
56
58
|
NoUnreachableAfterTerminal,
|
|
57
59
|
)
|
|
@@ -73,12 +75,14 @@ from sarj_python_lint.rules.prefer_module_level_constant import (
|
|
|
73
75
|
from sarj_python_lint.rules.prefer_namedtuple_over_tuple_return import (
|
|
74
76
|
PreferNamedtupleOverTupleReturn,
|
|
75
77
|
)
|
|
78
|
+
from sarj_python_lint.rules.prefer_nominal_id_types import PreferNominalIdTypes
|
|
76
79
|
from sarj_python_lint.rules.prefer_non_nullable_collection import (
|
|
77
80
|
PreferNonNullableCollection,
|
|
78
81
|
)
|
|
79
82
|
from sarj_python_lint.rules.prefer_or_pattern import PreferOrPattern
|
|
80
83
|
from sarj_python_lint.rules.prefer_real_store_in_tests import PreferRealStoreInTests
|
|
81
84
|
from sarj_python_lint.rules.prefer_self_type_annotation import PreferSelfTypeAnnotation
|
|
85
|
+
from sarj_python_lint.rules.prefer_single_sentence_comment import PreferSingleSentenceComment
|
|
82
86
|
from sarj_python_lint.rules.prefer_str_enum import PreferStrEnum
|
|
83
87
|
from sarj_python_lint.rules.prefer_struct_over_namedtuple import (
|
|
84
88
|
PreferStructOverNamedtuple,
|
|
@@ -190,6 +194,10 @@ REGISTRY: dict[str, type[Rule]] = {
|
|
|
190
194
|
DocstringReturnsRestateSignature.id: DocstringReturnsRestateSignature,
|
|
191
195
|
RestatedTestDocstring.id: RestatedTestDocstring,
|
|
192
196
|
TestPhaseLabelComment.id: TestPhaseLabelComment,
|
|
197
|
+
PreferSingleSentenceComment.id: PreferSingleSentenceComment,
|
|
198
|
+
NoLongComment.id: NoLongComment,
|
|
199
|
+
NoTypedDocSections.id: NoTypedDocSections,
|
|
200
|
+
PreferNominalIdTypes.id: PreferNominalIdTypes,
|
|
193
201
|
}
|
|
194
202
|
|
|
195
203
|
__all__ = ["REGISTRY"]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Extract executable SQL while masking quoted values and comments."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from sarj_python_lint.rules._paths import is_test_path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_store_module(path: Path) -> bool:
|
|
16
|
+
"""Return whether a non-test path belongs to the store layer."""
|
|
17
|
+
if is_test_path(path):
|
|
18
|
+
return False
|
|
19
|
+
return path.name.endswith("_store.py") or "stores" in path.parts
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def sql_string_value(node: ast.expr) -> str | None:
|
|
23
|
+
"""Reconstruct a (possibly `+`-concatenated) string literal, else None."""
|
|
24
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
25
|
+
return node.value
|
|
26
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
|
|
27
|
+
left = sql_string_value(node.left)
|
|
28
|
+
right = sql_string_value(node.right)
|
|
29
|
+
if left is not None and right is not None:
|
|
30
|
+
return left + right
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def strip_sql_noise(text: str) -> str:
|
|
35
|
+
"""Mask SQL values and comments without changing text or line lengths."""
|
|
36
|
+
out = list(text)
|
|
37
|
+
n = len(text)
|
|
38
|
+
i = 0
|
|
39
|
+
while i < n:
|
|
40
|
+
ch = text[i]
|
|
41
|
+
if ch in {"'", '"'}:
|
|
42
|
+
out[i] = " "
|
|
43
|
+
i += 1
|
|
44
|
+
while i < n:
|
|
45
|
+
c = text[i]
|
|
46
|
+
if c == ch:
|
|
47
|
+
if i + 1 < n and text[i + 1] == ch:
|
|
48
|
+
out[i] = out[i + 1] = " "
|
|
49
|
+
i += 2
|
|
50
|
+
continue
|
|
51
|
+
out[i] = " "
|
|
52
|
+
i += 1
|
|
53
|
+
break
|
|
54
|
+
if c != "\n":
|
|
55
|
+
out[i] = " "
|
|
56
|
+
i += 1
|
|
57
|
+
continue
|
|
58
|
+
if ch == "-" and i + 1 < n and text[i + 1] == "-":
|
|
59
|
+
while i < n and text[i] != "\n":
|
|
60
|
+
out[i] = " "
|
|
61
|
+
i += 1
|
|
62
|
+
continue
|
|
63
|
+
if ch == "/" and i + 1 < n and text[i + 1] == "*":
|
|
64
|
+
out[i] = out[i + 1] = " "
|
|
65
|
+
i += 2
|
|
66
|
+
while i < n and not (text[i] == "*" and i + 1 < n and text[i + 1] == "/"):
|
|
67
|
+
if text[i] != "\n":
|
|
68
|
+
out[i] = " "
|
|
69
|
+
i += 1
|
|
70
|
+
if i < n:
|
|
71
|
+
out[i] = " "
|
|
72
|
+
i += 1
|
|
73
|
+
if i < n:
|
|
74
|
+
out[i] = " "
|
|
75
|
+
i += 1
|
|
76
|
+
continue
|
|
77
|
+
i += 1
|
|
78
|
+
return "".join(out)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""SARJ065 — A test whose every assertion sits behind a branch or loop can pass asserting nothing.
|
|
2
2
|
|
|
3
3
|
Examples: https://github.com/sarj-ai/standards/blob/main/packages/python/tests/rules/test_conditional_assertion_in_test.py
|
|
4
|
-
Evidence: https://github.com/sarj-ai/standards/blob/main/docs/rules/SARJ065.md
|
|
5
4
|
"""
|
|
6
5
|
|
|
7
6
|
from __future__ import annotations
|
|
@@ -137,7 +136,6 @@ class _Facts:
|
|
|
137
136
|
class ConditionalAssertionInTest(Rule):
|
|
138
137
|
id: str = "conditional-assertion-in-test"
|
|
139
138
|
code: str = "SARJ065"
|
|
140
|
-
has_evidence: bool = True
|
|
141
139
|
description: str = "Every assertion in the test is inside a conditional or loop — it can pass asserting nothing."
|
|
142
140
|
|
|
143
141
|
@override
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""SARJ086 — An `Args:` block that only re-spells the parameter list.
|
|
2
2
|
|
|
3
3
|
Examples: https://github.com/sarj-ai/standards/blob/main/packages/python/tests/rules/test_docstring_args_restate_signature.py
|
|
4
|
-
Evidence: https://github.com/sarj-ai/standards/blob/main/docs/rules/SARJ086.md
|
|
5
4
|
"""
|
|
6
5
|
|
|
7
6
|
from __future__ import annotations
|
|
@@ -32,7 +31,6 @@ if TYPE_CHECKING:
|
|
|
32
31
|
class DocstringArgsRestateSignature(Rule):
|
|
33
32
|
id: str = "docstring-args-restate-signature"
|
|
34
33
|
code: str = "SARJ086"
|
|
35
|
-
has_evidence: bool = True
|
|
36
34
|
description: str = (
|
|
37
35
|
"`Args:` block adds nothing the signature does not already say — delete the section and keep the summary."
|
|
38
36
|
)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""SARJ087 — A `Returns:` block that only re-spells the name and the return annotation.
|
|
2
2
|
|
|
3
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
4
|
"""
|
|
6
5
|
|
|
7
6
|
from __future__ import annotations
|
|
@@ -52,7 +51,6 @@ def _return_block(docstring: str) -> str | None:
|
|
|
52
51
|
class DocstringReturnsRestateSignature(Rule):
|
|
53
52
|
id: str = "docstring-returns-restate-signature"
|
|
54
53
|
code: str = "SARJ087"
|
|
55
|
-
has_evidence: bool = True
|
|
56
54
|
description: str = (
|
|
57
55
|
"`Returns:` block adds nothing the name and the return annotation do not "
|
|
58
56
|
"already say — delete the section and keep the summary."
|