sarj-python-lint 0.40.0__tar.gz → 0.41.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 (96) hide show
  1. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/_comments.py +164 -0
  4. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_comment_cruft.py +17 -0
  5. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_restated_comment.py +7 -1
  6. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/.gitignore +0 -0
  7. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/README.md +0 -0
  8. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/__init__.py +0 -0
  9. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/__main__.py +0 -0
  10. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  11. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/_secret_names.py +0 -0
  12. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/_version.py +0 -0
  13. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/py.typed +0 -0
  14. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/ratchet.py +0 -0
  15. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rule_base.py +0 -0
  16. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  17. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  18. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  19. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  20. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  21. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  22. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  23. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/_registry.py +0 -0
  24. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  25. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  26. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/conditional_assertion_in_test.py +0 -0
  27. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  28. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  29. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/duplicate_test_body.py +0 -0
  30. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/duplicated_override_docstring.py +0 -0
  31. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
  32. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/inefficient_string_concat_in_loop.py +0 -0
  33. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/interaction_only_test.py +0 -0
  34. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
  35. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/kwonly_same_type_params.py +0 -0
  36. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  37. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
  38. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  39. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_fat_try_blocks.py +0 -0
  40. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
  41. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -0
  42. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  43. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_fstring_in_log.py +0 -0
  44. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +0 -0
  45. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
  46. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  47. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
  48. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
  49. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_raw_sql_in_tests.py +0 -0
  50. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  51. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  52. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  53. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
  54. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_sequential_await.py +0 -0
  55. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_sleep_in_test_body.py +0 -0
  56. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
  57. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  58. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/no_unreachable_after_terminal.py +0 -0
  59. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  60. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/parametrize_case_needs_id.py +0 -0
  61. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  62. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  63. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  64. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  65. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  66. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  67. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/prefer_match_pattern_destructuring.py +0 -0
  68. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  69. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  70. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  71. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  72. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  73. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/prefer_real_store_in_tests.py +0 -0
  74. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  75. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  76. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  77. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
  78. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  79. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  80. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  81. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
  82. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  83. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  84. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  85. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  86. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/single_public_export.py +0 -0
  87. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/sleep_with_computed_arg_in_test.py +0 -0
  88. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  89. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  90. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/tautological_mock_assertion.py +0 -0
  91. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/test_loops_over_literal_cases.py +0 -0
  92. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  93. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
  94. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  95. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.0}/src/sarj_python_lint/rules/xfail_requires_strict.py +0 -0
  96. {sarj_python_lint-0.40.0 → sarj_python_lint-0.41.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.40.0
3
+ Version: 0.41.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "0.40.0"
3
+ version = "0.41.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" }]
@@ -26,14 +26,18 @@ tokenize each file once between them rather than once each.
26
26
 
27
27
  from __future__ import annotations
28
28
 
29
+ import ast
29
30
  import io
30
31
  import re
31
32
  import tokenize
32
33
  from typing import TYPE_CHECKING
33
34
 
35
+ from sarj_python_lint.rule_base import parse_or_none
36
+
34
37
 
35
38
  if TYPE_CHECKING:
36
39
  from collections.abc import Iterable, Sequence
40
+ from pathlib import Path
37
41
 
38
42
 
39
43
  # S1 — external reference: URL, issue/ticket key, RFC/PEP/CVE, bare GitHub issue
@@ -476,3 +480,163 @@ def comment_runs(standalone: Sequence[tuple[int, int, str]]) -> list[list[tuple[
476
480
  else:
477
481
  runs.append([entry])
478
482
  return runs
483
+
484
+
485
+ # A comment wall is judged as a block, not as an isolated sentence. AI-written
486
+ # walkthroughs commonly evade the short-comment restatement rule by adding one
487
+ # or two filler words to every step; repetition is the evidence in that shape.
488
+ _WALL_MIN_STATEMENTS = 4
489
+ _WALL_MIN_COMMENTS = 3
490
+ _WALL_MIN_COMMENTED_RATIO = 0.6
491
+ _WALL_MIN_WEAK_RATIO = 0.75
492
+ _WALL_MAX_WORDS = 18
493
+ _WALL_MIN_CONTENT_WORDS = 1
494
+ _WALL_MIN_MATCHED_RATIO = 0.5
495
+ _WALL_MAX_NOVEL_WORDS = 2
496
+ _WALL_DIRECTIVE_RE = re.compile(
497
+ r"^(?:todo|fixme|hack\b|xxx|note:|nb:|warning:|important:|noqa|sarj-noqa|"
498
+ r"type:|pragma|pyright|mypy|fmt:|isort|ruff|pylint|flake8|nosec|nosemgrep|"
499
+ r"-\*-|!/|coding[:=])",
500
+ re.IGNORECASE,
501
+ )
502
+ _WALL_NARRATION_RE = re.compile(
503
+ r"^(?:first(?:ly)?|second(?:ly)?|third(?:ly)?|then|next|now|finally|lastly|"
504
+ r"add|append|assign|await|build|calculate|call|check|clear|close|compute|convert|copy|"
505
+ r"count|create|declare|define|delete|extract|fetch|filter|find|format|generate|get|"
506
+ r"handle|initialize|insert|iterate|join|load|log|loop|map|merge|open|parse|print|"
507
+ r"process|push|read|remove|render|reset|return|save|send|set|setup|sort|split|"
508
+ r"start|stop|store|update|validate|wrap|write|apply|assemble|coerce|compress|"
509
+ r"disable|enable|lint|populate|prepare|redirect|register|resolve|run|stash|use)\b",
510
+ re.IGNORECASE,
511
+ )
512
+ _WALL_STATEMENTS = (
513
+ ast.Assign,
514
+ ast.AnnAssign,
515
+ ast.AugAssign,
516
+ ast.Assert,
517
+ ast.Delete,
518
+ ast.Expr,
519
+ ast.Raise,
520
+ ast.Return,
521
+ ast.If,
522
+ ast.For,
523
+ ast.AsyncFor,
524
+ ast.While,
525
+ ast.With,
526
+ ast.AsyncWith,
527
+ ast.Match,
528
+ ast.Try,
529
+ ast.TryStar,
530
+ )
531
+
532
+
533
+ def _is_wall_statement(node: ast.stmt) -> bool:
534
+ """Exclude docstrings while retaining executable expression statements."""
535
+ return isinstance(node, _WALL_STATEMENTS) and not (
536
+ isinstance(node, ast.Expr)
537
+ and isinstance(node.value, ast.Constant)
538
+ and isinstance(node.value.value, str)
539
+ )
540
+
541
+
542
+ def _weak_walkthrough_comment(body: str, statement: str) -> bool:
543
+ """Whether a comment is weak evidence inside a repeated walkthrough."""
544
+ if (
545
+ not body
546
+ or body.endswith("?")
547
+ or len(body.split()) > _WALL_MAX_WORDS
548
+ or _WALL_DIRECTIVE_RE.match(body)
549
+ or is_protected(body)
550
+ ):
551
+ return False
552
+ words = content_tokens(body)
553
+ if len(words) < _WALL_MIN_CONTENT_WORDS or not _WALL_NARRATION_RE.match(body):
554
+ return False
555
+ # The opener describes the operation and need not literally occur in the
556
+ # statement (`Fetch users` / `users = store.list()`). The remainder must be
557
+ # mostly corroborated; at most two filler words may be novel.
558
+ known = code_tokens(statement)
559
+ described = words[1:]
560
+ if not described:
561
+ return restates(words, known)
562
+ matched = sum(1 for word in described if restates([word], known))
563
+ novel = len(described) - matched
564
+ return matched / len(described) >= _WALL_MIN_MATCHED_RATIO and novel <= _WALL_MAX_NOVEL_WORDS
565
+
566
+
567
+ def _owned_statement_lists(owner: ast.AST) -> tuple[list[ast.stmt], ...]:
568
+ """Return the distinct statement blocks directly owned by an AST node."""
569
+ if isinstance(
570
+ owner,
571
+ (
572
+ ast.Module,
573
+ ast.FunctionDef,
574
+ ast.AsyncFunctionDef,
575
+ ast.ClassDef,
576
+ ast.With,
577
+ ast.AsyncWith,
578
+ ast.ExceptHandler,
579
+ ),
580
+ ):
581
+ return (owner.body,)
582
+ if isinstance(owner, (ast.If, ast.For, ast.AsyncFor, ast.While)):
583
+ return owner.body, owner.orelse
584
+ if isinstance(owner, (ast.Try, ast.TryStar)):
585
+ return owner.body, owner.orelse, owner.finalbody
586
+ if isinstance(owner, ast.match_case):
587
+ return (owner.body,)
588
+ return ()
589
+
590
+
591
+ def statement_comment_walls(
592
+ path: Path,
593
+ source: str,
594
+ standalone: Sequence[tuple[int, int, str]],
595
+ ) -> dict[int, frozenset[int]]:
596
+ """Return `{leader: member lines}` for repeated statement narration walls.
597
+
598
+ Each wall lives within one AST statement list (module, function, branch,
599
+ loop, handler). A comment must be directly above and aligned with the simple
600
+ statement it describes. Three weak comments are not enough by themselves:
601
+ comments must cover most of a block, and most attached comments must be
602
+ weak. This keeps an occasional label or a carefully documented exceptional
603
+ step from turning a whole function into a finding.
604
+
605
+ """
606
+ tree = parse_or_none(path, source)
607
+ if tree is None:
608
+ return {}
609
+ comments = {line: (col, body) for line, col, body in standalone}
610
+ walls: dict[int, frozenset[int]] = {}
611
+ for owner in ast.walk(tree):
612
+ for value in _owned_statement_lists(owner):
613
+ statements = [item for item in value if _is_wall_statement(item)]
614
+ if len(statements) < _WALL_MIN_STATEMENTS:
615
+ continue
616
+ attached: list[tuple[int, int, bool]] = []
617
+ for index, statement in enumerate(statements):
618
+ entry = comments.get(statement.lineno - 1)
619
+ if entry is None or entry[0] != statement.col_offset:
620
+ continue
621
+ line = statement.lineno - 1
622
+ segment = ast.get_source_segment(source, statement) or ""
623
+ attached.append((index, line, _weak_walkthrough_comment(entry[1], segment)))
624
+ clusters: list[list[tuple[int, int, bool]]] = []
625
+ for item in attached:
626
+ if clusters and item[0] <= clusters[-1][-1][0] + 2:
627
+ clusters[-1].append(item)
628
+ else:
629
+ clusters.append([item])
630
+ for cluster in clusters:
631
+ span = cluster[-1][0] - cluster[0][0] + 1
632
+ weak = [line for _index, line, is_weak in cluster if is_weak]
633
+ if (
634
+ span < _WALL_MIN_STATEMENTS
635
+ or len(weak) < _WALL_MIN_COMMENTS
636
+ or len(cluster) / span < _WALL_MIN_COMMENTED_RATIO
637
+ or len(weak) / len(cluster) < _WALL_MIN_WEAK_RATIO
638
+ ):
639
+ continue
640
+ leader = min(weak)
641
+ walls[leader] = frozenset(weak)
642
+ return walls
@@ -17,6 +17,7 @@ from sarj_python_lint.rules._comments import (
17
17
  has_external_reference,
18
18
  nested_comment_lines,
19
19
  standalone_comments,
20
+ statement_comment_walls,
20
21
  )
21
22
  from sarj_python_lint.rules._paths import is_generated
22
23
 
@@ -503,7 +504,23 @@ class NoCommentCruft(Rule):
503
504
  nested = nested_comment_lines(source)
504
505
  enumerated = [line for line, _, body in standalone if _ENUMERATION_RE.match(body)]
505
506
  license_header = _license_header_lines(standalone)
507
+ walls = statement_comment_walls(path, source, standalone)
508
+ wall_members = frozenset(line for members in walls.values() for line in members)
506
509
  for line, col, body in standalone:
510
+ if line in walls:
511
+ diags[line] = Diagnostic(
512
+ path=path,
513
+ line=line,
514
+ col=col + 1,
515
+ code=self.code,
516
+ message=(
517
+ f"Statement comment wall ({len(walls[line])} narrated steps) — "
518
+ "delete the walkthrough and name the operations in code; keep only constraints or rationale."
519
+ ),
520
+ )
521
+ continue
522
+ if line in wall_members:
523
+ continue
507
524
  if _is_directive(body) or _is_coding_cookie(body) or line in skip:
508
525
  continue
509
526
  prev_body = by_line.get(line - 1)
@@ -20,6 +20,7 @@ from sarj_python_lint.rules._comments import (
20
20
  nested_comment_lines,
21
21
  restates,
22
22
  standalone_comments,
23
+ statement_comment_walls,
23
24
  )
24
25
  from sarj_python_lint.rules._paths import is_generated
25
26
 
@@ -261,12 +262,17 @@ class NoRestatedComment(Rule):
261
262
  except tokenize.TokenError, IndentationError, SyntaxError:
262
263
  return []
263
264
  lines = source.splitlines()
265
+ wall_members = frozenset(
266
+ line
267
+ for members in statement_comment_walls(path, source, standalone).values()
268
+ for line in members
269
+ )
264
270
  diags: list[Diagnostic] = []
265
271
  for run in comment_runs(standalone):
266
272
  if len(run) != 1:
267
273
  continue
268
274
  line, col, body = run[0]
269
- if line in nested:
275
+ if line in nested or line in wall_members:
270
276
  continue
271
277
  if self._restates_below(body, line, lines):
272
278
  diags.append(