sarj-python-lint 0.44.0__tar.gz → 0.46.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 (103) hide show
  1. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/PKG-INFO +26 -2
  2. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/README.md +25 -1
  3. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/pyproject.toml +1 -1
  4. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/__main__.py +1 -0
  5. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_comments.py +2 -1
  6. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_prose_budget.py +17 -2
  7. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_registry.py +4 -0
  8. sarj_python_lint-0.46.0/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +713 -0
  9. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/.gitignore +0 -0
  10. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/__init__.py +0 -0
  11. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  12. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/_secret_names.py +0 -0
  13. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/_version.py +0 -0
  14. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/py.typed +0 -0
  15. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/ratchet.py +0 -0
  16. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rule_base.py +0 -0
  17. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  18. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  19. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  20. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  21. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  22. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  23. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  24. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  25. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  26. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  27. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/conditional_assertion_in_test.py +0 -0
  28. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  29. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  30. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/duplicate_test_body.py +0 -0
  31. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/duplicated_override_docstring.py +0 -0
  32. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/fastapi_openapi_contract.py +0 -0
  33. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
  34. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/inefficient_string_concat_in_loop.py +0 -0
  35. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/interaction_only_test.py +0 -0
  36. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
  37. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/kwonly_same_type_params.py +0 -0
  38. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  39. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
  40. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  41. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  42. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_fat_try_blocks.py +0 -0
  43. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
  44. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -0
  45. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  46. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_fstring_in_log.py +0 -0
  47. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +0 -0
  48. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
  49. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_long_comment.py +0 -0
  50. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  51. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
  52. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
  53. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_raw_sql_in_tests.py +0 -0
  54. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  55. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  56. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  57. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  58. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
  59. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_sequential_await.py +0 -0
  60. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_sleep_in_test_body.py +0 -0
  61. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
  62. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  63. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  64. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/no_unreachable_after_terminal.py +0 -0
  65. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  66. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/parametrize_case_needs_id.py +0 -0
  67. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  68. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  69. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  70. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  71. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  72. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  73. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  74. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  75. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  76. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  77. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  78. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  79. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_real_store_in_tests.py +0 -0
  80. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  81. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_single_sentence_comment.py +0 -0
  82. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  83. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  84. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
  85. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  86. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  87. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  88. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
  89. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  90. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  91. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  92. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  93. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/single_public_export.py +0 -0
  94. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/sleep_with_computed_arg_in_test.py +0 -0
  95. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  96. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  97. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/tautological_mock_assertion.py +0 -0
  98. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/test_loops_over_literal_cases.py +0 -0
  99. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  100. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
  101. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  102. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.0}/src/sarj_python_lint/rules/xfail_requires_strict.py +0 -0
  103. {sarj_python_lint-0.44.0 → sarj_python_lint-0.46.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.44.0
3
+ Version: 0.46.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
@@ -28,7 +28,7 @@ uv tool install sarj-python-lint
28
28
 
29
29
  ```yaml
30
30
  - repo: https://github.com/sarj-ai/standards
31
- rev: python-v0.44.0
31
+ rev: python-v0.46.0
32
32
  hooks:
33
33
  - id: sarj-no-sequential-await
34
34
  - id: sarj-inefficient-string-concat-in-loop
@@ -36,6 +36,7 @@ uv tool install sarj-python-lint
36
36
  - id: sarj-no-fat-try-blocks
37
37
  - id: sarj-pydantic-at-boundaries
38
38
  - id: sarj-fastapi-openapi-contract # SARJ094
39
+ - id: sarj-no-hidden-constructor-fallback # SARJ095 (warning)
39
40
  - id: sarj-prefer-class-row
40
41
  - id: sarj-prefer-timedelta-for-durations
41
42
  - id: sarj-prefer-struct-over-namedtuple
@@ -61,6 +62,29 @@ function-local framework imports, imported router instances and the assembled
61
62
  Existing projects can adopt the default-enabled rule with `--update-baseline`
62
63
  and then shrink that baseline as endpoint contracts are repaired.
63
64
 
65
+ ### Hidden constructor settings fallback (0.45.0)
66
+
67
+ `SARJ095` warns when a keyword-only constructor parameter defaults to `None`
68
+ and the constructor silently replaces it with a proven pydantic-settings value.
69
+ The effective dependency is invisible at the call site, and `value or
70
+ settings.VALUE` also treats an explicit falsey value as omitted. Make the
71
+ argument required and resolve the setting at the application composition root;
72
+ the annotation may remain nullable when `None` is still a valid explicit value.
73
+
74
+ The rule resolves same-module settings objects, imports, aliases and re-exports
75
+ back to an instance of a `pydantic_settings.BaseSettings` subclass. Literal and
76
+ enum defaults, mutable-container initialization, arbitrary factories and
77
+ clients, module constants, environment-variable APIs, other parameters and
78
+ instance state are deliberately outside v1. No autofix is offered because
79
+ changing constructor optionality requires coordinated call-site edits.
80
+
81
+ Measured over 4,638 tracked Python files in 33 first-party repositories: three
82
+ constructor warnings across two repositories, all three actionable. A pinned
83
+ 15-repository OSS sweep covered 29,203 files and produced zero reports, which is
84
+ compatibility evidence rather than a precision claim. An environment-variable
85
+ arm was rejected before shipping: it reported a public first-party library and
86
+ two intentional LiteLLM integration constructors, all non-actionable.
87
+
64
88
  ### Test-quality rules (0.15.0)
65
89
 
66
90
  Mined from an AST audit of ~7,500 test functions across two production repos.
@@ -10,7 +10,7 @@ uv tool install sarj-python-lint
10
10
 
11
11
  ```yaml
12
12
  - repo: https://github.com/sarj-ai/standards
13
- rev: python-v0.44.0
13
+ rev: python-v0.46.0
14
14
  hooks:
15
15
  - id: sarj-no-sequential-await
16
16
  - id: sarj-inefficient-string-concat-in-loop
@@ -18,6 +18,7 @@ uv tool install sarj-python-lint
18
18
  - id: sarj-no-fat-try-blocks
19
19
  - id: sarj-pydantic-at-boundaries
20
20
  - id: sarj-fastapi-openapi-contract # SARJ094
21
+ - id: sarj-no-hidden-constructor-fallback # SARJ095 (warning)
21
22
  - id: sarj-prefer-class-row
22
23
  - id: sarj-prefer-timedelta-for-durations
23
24
  - id: sarj-prefer-struct-over-namedtuple
@@ -43,6 +44,29 @@ function-local framework imports, imported router instances and the assembled
43
44
  Existing projects can adopt the default-enabled rule with `--update-baseline`
44
45
  and then shrink that baseline as endpoint contracts are repaired.
45
46
 
47
+ ### Hidden constructor settings fallback (0.45.0)
48
+
49
+ `SARJ095` warns when a keyword-only constructor parameter defaults to `None`
50
+ and the constructor silently replaces it with a proven pydantic-settings value.
51
+ The effective dependency is invisible at the call site, and `value or
52
+ settings.VALUE` also treats an explicit falsey value as omitted. Make the
53
+ argument required and resolve the setting at the application composition root;
54
+ the annotation may remain nullable when `None` is still a valid explicit value.
55
+
56
+ The rule resolves same-module settings objects, imports, aliases and re-exports
57
+ back to an instance of a `pydantic_settings.BaseSettings` subclass. Literal and
58
+ enum defaults, mutable-container initialization, arbitrary factories and
59
+ clients, module constants, environment-variable APIs, other parameters and
60
+ instance state are deliberately outside v1. No autofix is offered because
61
+ changing constructor optionality requires coordinated call-site edits.
62
+
63
+ Measured over 4,638 tracked Python files in 33 first-party repositories: three
64
+ constructor warnings across two repositories, all three actionable. A pinned
65
+ 15-repository OSS sweep covered 29,203 files and produced zero reports, which is
66
+ compatibility evidence rather than a precision claim. An environment-variable
67
+ arm was rejected before shipping: it reported a public first-party library and
68
+ two intentional LiteLLM integration constructors, all non-actionable.
69
+
46
70
  ### Test-quality rules (0.15.0)
47
71
 
48
72
  Mined from an AST audit of ~7,500 test functions across two production repos.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "0.44.0"
3
+ version = "0.46.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" }]
@@ -25,6 +25,7 @@ SKIP_DIR_NAMES = {
25
25
  "__pycache__",
26
26
  ".pytest_cache",
27
27
  ".ruff_cache",
28
+ ".uv-cache",
28
29
  ".mypy_cache",
29
30
  ".turbo",
30
31
  ".yarn",
@@ -9,6 +9,7 @@ import tokenize
9
9
  from typing import TYPE_CHECKING, NamedTuple
10
10
 
11
11
  from sarj_python_lint.rule_base import parse_or_none
12
+ from sarj_python_lint.rules._ast_index import nodes
12
13
 
13
14
 
14
15
  if TYPE_CHECKING:
@@ -498,7 +499,7 @@ def statement_comment_walls(
498
499
  return {}
499
500
  comments = {line: (col, body) for line, col, body in standalone}
500
501
  walls: dict[int, frozenset[int]] = {}
501
- for owner in ast.walk(tree):
502
+ for owner in nodes(tree, ast.AST):
502
503
  for value in _owned_statement_lists(owner):
503
504
  statements = [item for item in value if _is_wall_statement(item)]
504
505
  if len(statements) < _WALL_MIN_STATEMENTS:
@@ -10,6 +10,7 @@ import tokenize
10
10
  from typing import TYPE_CHECKING, Final
11
11
 
12
12
  from sarj_python_lint.rule_base import parse_or_none
13
+ from sarj_python_lint.rules._ast_index import nodes
13
14
  from sarj_python_lint.rules._docstrings import PROMPT_DECORATOR_MARKERS, decorator_markers, sections
14
15
  from sarj_python_lint.rules._paths import is_generated
15
16
 
@@ -53,6 +54,9 @@ class ProseGroup:
53
54
  typed_sections: frozenset[str] = frozenset()
54
55
 
55
56
 
57
+ _last_groups: tuple[str, str, tuple[ProseGroup, ...]] | None = None
58
+
59
+
56
60
  def sentence_units(text: str) -> int:
57
61
  """Count prose sentences and unpunctuated list items deterministically."""
58
62
  text = _without_examples_metadata(text)
@@ -98,6 +102,17 @@ def _without_examples_metadata(text: str) -> str:
98
102
 
99
103
  def groups(path: Path, source: str) -> list[ProseGroup]:
100
104
  """Extract docstrings and contiguous own-line comment runs from one file."""
105
+ global _last_groups # ruff: ignore[global-statement] -- rules run sequentially per file.
106
+ path_key = str(path)
107
+ if _last_groups is not None and _last_groups[0] == path_key and _last_groups[1] is source:
108
+ return list(_last_groups[2])
109
+ extracted = _extract_groups(path, source)
110
+ _last_groups = (path_key, source, tuple(extracted))
111
+ return extracted
112
+
113
+
114
+ def _extract_groups(path: Path, source: str) -> list[ProseGroup]:
115
+ """Perform the shared parse and tokenization once for adjacent prose rules."""
101
116
  if is_generated(path, source):
102
117
  return []
103
118
  tree = parse_or_none(path, source)
@@ -140,8 +155,8 @@ def _comment_run(run: list[tokenize.TokenInfo]) -> ProseGroup:
140
155
 
141
156
  def _docstring_groups(tree: ast.Module) -> list[ProseGroup]:
142
157
  out: list[ProseGroup] = []
143
- for node in ast.walk(tree):
144
- if not isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)) or not node.body:
158
+ for node in nodes(tree, ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef):
159
+ if not node.body:
145
160
  continue
146
161
  first = node.body[0]
147
162
  if not (
@@ -37,6 +37,9 @@ from sarj_python_lint.rules.no_first_party_private_import import (
37
37
  )
38
38
  from sarj_python_lint.rules.no_fstring_in_log import NoFstringInLog
39
39
  from sarj_python_lint.rules.no_gen_random_uuid_in_sql import NoGenRandomUuidInSql
40
+ from sarj_python_lint.rules.no_hidden_constructor_fallback import (
41
+ NoHiddenConstructorFallback,
42
+ )
40
43
  from sarj_python_lint.rules.no_isinstance_union_chain import NoIsinstanceUnionChain
41
44
  from sarj_python_lint.rules.no_long_comment import NoLongComment
42
45
  from sarj_python_lint.rules.no_offset_pagination import NoOffsetPagination
@@ -167,6 +170,7 @@ REGISTRY: dict[str, type[Rule]] = {
167
170
  TrailingValueNarration.id: TrailingValueNarration,
168
171
  NoStdlibLogging.id: NoStdlibLogging,
169
172
  NoGenRandomUuidInSql.id: NoGenRandomUuidInSql,
173
+ NoHiddenConstructorFallback.id: NoHiddenConstructorFallback,
170
174
  NoFileLevelEscapeHatchNoqa.id: NoFileLevelEscapeHatchNoqa,
171
175
  NoOptionalTenantPredicate.id: NoOptionalTenantPredicate,
172
176
  NoTautologicalExpect.id: NoTautologicalExpect,