sarj-python-lint 0.82.1__tar.gz → 0.82.3__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 (131) hide show
  1. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/PKG-INFO +1 -1
  2. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/pyproject.toml +1 -1
  3. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/__main__.py +53 -33
  4. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/_python_target.py +3 -0
  5. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/_ratchet_cli.py +4 -0
  6. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/ratchet.py +33 -23
  7. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rule_base.py +3 -0
  8. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/_comments.py +37 -26
  9. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/_fastapi.py +230 -170
  10. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/_first_party.py +11 -5
  11. sarj_python_lint-0.82.3/src/sarj_python_lint/rules/_fixed_record.py +170 -0
  12. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/_imports.py +19 -15
  13. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/_paths.py +11 -6
  14. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/_project_index.py +172 -141
  15. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/_prose_budget.py +145 -133
  16. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/_registry.py +2 -0
  17. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/_sql.py +38 -58
  18. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +231 -177
  19. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +12 -6
  20. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +216 -140
  21. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +394 -349
  22. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/mock_without_spec.py +48 -51
  23. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +17 -8
  24. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_comment_cruft.py +66 -54
  25. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_deleted_only_override_parameter.py +10 -6
  26. sarj_python_lint-0.82.3/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +262 -0
  27. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_first_party_private_import.py +11 -5
  28. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +20 -14
  29. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_generic_single_export_module.py +9 -5
  30. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +144 -100
  31. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +25 -23
  32. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +235 -171
  33. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +118 -70
  34. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +11 -6
  35. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +9 -5
  36. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +54 -40
  37. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_redundant_literal_description.py +38 -30
  38. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_repeated_string_literal.py +43 -24
  39. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_repeated_test_body.py +65 -44
  40. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_restated_comment.py +46 -32
  41. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_secret_in_log.py +23 -17
  42. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +18 -12
  43. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +37 -21
  44. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_typed_doc_sections.py +10 -6
  45. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +25 -15
  46. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +167 -144
  47. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +9 -5
  48. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +25 -17
  49. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/over_mocked_test.py +25 -15
  50. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/prefer_class_row.py +26 -17
  51. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +18 -13
  52. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +119 -79
  53. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +38 -28
  54. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/prefer_library_fake.py +42 -30
  55. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/prefer_match_assert_never.py +33 -28
  56. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +36 -32
  57. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +46 -31
  58. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/prefer_module_level_constant.py +14 -10
  59. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +101 -71
  60. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +49 -45
  61. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +33 -17
  62. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +8 -4
  63. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/prefer_str_enum.py +434 -313
  64. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +47 -26
  65. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +5 -1
  66. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +4 -1
  67. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +11 -7
  68. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +7 -5
  69. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/production_derived_test_cases.py +40 -26
  70. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +24 -16
  71. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +54 -34
  72. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/redundant_class_docstring.py +40 -31
  73. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/redundant_docstring.py +40 -31
  74. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/repeated_static_call_cases.py +27 -17
  75. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +54 -41
  76. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +175 -163
  77. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/require_port_for_service.py +190 -131
  78. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +204 -150
  79. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +272 -206
  80. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/restated_test_docstring.py +70 -48
  81. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/stepdown.py +145 -116
  82. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +24 -14
  83. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +72 -50
  84. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +63 -54
  85. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/typed_error_reasons.py +8 -8
  86. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +121 -90
  87. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/unused_mock_setup.py +121 -101
  88. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/unused_test_factory_option.py +95 -69
  89. sarj_python_lint-0.82.1/src/sarj_python_lint/rules/_fixed_record.py +0 -146
  90. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/.gitignore +0 -0
  91. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/LICENSE +0 -0
  92. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/README.md +0 -0
  93. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/__init__.py +0 -0
  94. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/_analysis_session.py +0 -0
  95. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/_filesystem.py +0 -0
  96. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/_secret_names.py +0 -0
  97. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/_version.py +0 -0
  98. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/py.typed +0 -0
  99. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/__init__.py +0 -0
  100. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  101. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  102. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/_logging.py +0 -0
  103. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/_pytest.py +0 -0
  104. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  105. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  106. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  107. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  108. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  109. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  110. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  111. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  112. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  113. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  114. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  115. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  116. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  117. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  118. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  119. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  120. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  121. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  122. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  123. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  124. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  125. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  126. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  127. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  128. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  129. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  130. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  131. {sarj_python_lint-0.82.1 → sarj_python_lint-0.82.3}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 0.82.1
3
+ Version: 0.82.3
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
6
6
  Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "0.82.1"
3
+ version = "0.82.3"
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" }]
@@ -14,7 +14,7 @@ import typer
14
14
  from sarj_python_lint import __version__
15
15
  from sarj_python_lint._analysis_session import AnalysisSession
16
16
  from sarj_python_lint._filesystem import atomic_write_text
17
- from sarj_python_lint.rule_base import Diagnostic, ProjectRule, Severity, is_suppressed
17
+ from sarj_python_lint.rule_base import Diagnostic, ProjectRule, Rule, Severity, is_suppressed
18
18
  from sarj_python_lint.rules import REGISTRY
19
19
  from sarj_python_lint.rules._paths import clear_path_caches
20
20
  from sarj_python_lint.rules._project_index import ProjectIndexSet
@@ -59,17 +59,23 @@ def _expand_paths(paths: list[Path]) -> list[Path]:
59
59
  except OSError:
60
60
  pass
61
61
  continue
62
- for child in p.rglob("*.py"):
63
- if not child.is_file():
64
- continue
65
- if any(part in SKIP_DIR_NAMES for part in child.parts):
66
- continue
67
- try:
68
- if child.stat().st_size > _MAX_FILE_BYTES:
69
- continue
70
- except OSError:
62
+ out.extend(_python_files(p))
63
+ return out
64
+
65
+
66
+ def _python_files(p: Path) -> list[Path]:
67
+ out: list[Path] = []
68
+ for child in p.rglob("*.py"):
69
+ if not child.is_file():
70
+ continue
71
+ if any(part in SKIP_DIR_NAMES for part in child.parts):
72
+ continue
73
+ try:
74
+ if child.stat().st_size > _MAX_FILE_BYTES:
71
75
  continue
72
- out.append(child)
76
+ except OSError:
77
+ continue
78
+ out.append(child)
73
79
  return out
74
80
 
75
81
 
@@ -98,22 +104,23 @@ def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
98
104
  rule.prepare(indexes)
99
105
  diags: list[Diagnostic] = []
100
106
  for p, source in loaded.items():
101
- source_lines = source.splitlines()
102
- raw = [diagnostic for rule in rules for diagnostic in rule.check(p, source)]
103
- diags.extend(
104
- diagnostic
105
- for diagnostic in deduplicate_diagnostics(
106
- [
107
- diagnostic
108
- for diagnostic in raw
109
- if diagnostic.code == "SARJ419" or not is_suppressed(source_lines, diagnostic.line, diagnostic.code)
110
- ],
111
- source=source,
112
- )
113
- )
107
+ diags.extend(_check_source(rules, p, source))
114
108
  return diags
115
109
 
116
110
 
111
+ def _check_source(rules: list[Rule], p: Path, source: str) -> list[Diagnostic]:
112
+ source_lines = source.splitlines()
113
+ raw = [diagnostic for rule in rules for diagnostic in rule.check(p, source)]
114
+ return deduplicate_diagnostics(
115
+ [
116
+ diagnostic
117
+ for diagnostic in raw
118
+ if diagnostic.code == "SARJ419" or not is_suppressed(source_lines, diagnostic.line, diagnostic.code)
119
+ ],
120
+ source=source,
121
+ )
122
+
123
+
117
124
  def analyze(
118
125
  rule_ids: list[str],
119
126
  paths: list[Path],
@@ -172,15 +179,7 @@ def deduplicate_diagnostics(diags: list[Diagnostic], *, source: str | None = Non
172
179
  location = owner_location(diagnostic)
173
180
  by_code = present.setdefault((diagnostic.path, location.line, location.column), {})
174
181
  by_code.setdefault(diagnostic.code, set()).add(diagnostic.severity)
175
- suppressed = {
176
- (location, generic, generic_severity)
177
- for location, codes in present.items()
178
- for specific, generics in _DIAGNOSTIC_PRECEDENCE.items()
179
- if specific in codes
180
- for generic in generics
181
- for generic_severity in codes.get(generic, set())
182
- if generic_severity is Severity.WARNING or Severity.ERROR in codes[specific]
183
- }
182
+ suppressed = _suppressed_diagnostics(present)
184
183
  return [
185
184
  diagnostic
186
185
  for diagnostic in diags
@@ -199,6 +198,27 @@ def deduplicate_diagnostics(diags: list[Diagnostic], *, source: str | None = Non
199
198
  ]
200
199
 
201
200
 
201
+ def _suppressed_diagnostics(
202
+ present: dict[tuple[Path, int, int], dict[str, set[Severity]]],
203
+ ) -> set[tuple[tuple[Path, int, int], str, Severity]]:
204
+ return {
205
+ (location, generic, severity)
206
+ for location, codes in present.items()
207
+ for generic, severity in _suppressed_codes(codes)
208
+ }
209
+
210
+
211
+ def _suppressed_codes(codes: dict[str, set[Severity]]) -> set[tuple[str, Severity]]:
212
+ return {
213
+ (generic, generic_severity)
214
+ for specific, generics in _DIAGNOSTIC_PRECEDENCE.items()
215
+ if specific in codes
216
+ for generic in generics
217
+ for generic_severity in codes.get(generic, set())
218
+ if generic_severity is Severity.WARNING or Severity.ERROR in codes[specific]
219
+ }
220
+
221
+
202
222
  def _function_signature_owner_locations(source: str) -> dict[int, _OwnerLocation]:
203
223
  try:
204
224
  tree = ast.parse(source)
@@ -53,6 +53,9 @@ class PythonTargetFacts:
53
53
  if root not in self._installed:
54
54
  self._installed[root] = _installed_declarations(root)
55
55
  return self._installed[root].get(resolved)
56
+ return self._project_target(resolved)
57
+
58
+ def _project_target(self, resolved: Path) -> str | None:
56
59
  for ancestor in list(resolved.parents)[:_MAX_ANCESTORS]:
57
60
  try:
58
61
  if any((ancestor / marker).exists() for marker in _PROJECT_BOUNDARIES):
@@ -111,6 +111,10 @@ def _run(args: _Options) -> int:
111
111
  allow_increase=args.allow_increase or not baseline_path.exists(),
112
112
  )
113
113
 
114
+ return _report_gate(measurement, baseline, packages)
115
+
116
+
117
+ def _report_gate(measurement: Measurement, baseline: Baseline, packages: list[str]) -> int:
114
118
  sys.stdout.write(
115
119
  f"{measurement.total} suppressions across {len(measurement.codes)} codes "
116
120
  f"in {len(packages)} package(s) (baseline total {sum(baseline.codes.values())})\n"
@@ -323,29 +323,7 @@ def _count_comment(
323
323
  standalone: bool,
324
324
  ruff_aliases: Mapping[str, str] | None,
325
325
  ) -> None:
326
- if standalone and (file_ignore := _RUFF_FILE_IGNORE_RE.match(comment)):
327
- counts.update(
328
- f"file-noqa:{_normalized_ruff_selector(code, ruff_aliases)}" for code in file_ignore.group(1).split(",")
329
- )
330
- return
331
- if standalone and (file_noqa := _FILE_NOQA_RE.match(comment)):
332
- listed = file_noqa.group(1)
333
- if listed:
334
- counts.update(f"file-noqa:{_normalized_ruff_selector(code, ruff_aliases)}" for code in listed.split(","))
335
- else:
336
- counts[_BLANKET_KEY] += 1
337
- return
338
- if standalone and _FLAKE8_FILE_NOQA_RE.match(comment):
339
- counts[_FLAKE8_BLANKET_KEY] += 1
340
- return
341
- if standalone and (disabled := _RUFF_DISABLE_RE.match(comment)):
342
- counts.update(
343
- f"ruff-range:{_normalized_ruff_selector(code, ruff_aliases)}" for code in disabled.group(1).split(",")
344
- )
345
- return
346
- if standalone and _FILE_PYRIGHT_RE.match(comment):
347
- rules: list[str] = _FILE_PYRIGHT_RULE_RE.findall(comment)
348
- counts.update(f"file-pyright:{rule}" for rule in rules)
326
+ if standalone and _count_file_comment(comment, counts, ruff_aliases=ruff_aliases):
349
327
  return
350
328
  for match in _NOQA_RE.finditer(comment):
351
329
  listed = match.group(1)
@@ -358,6 +336,10 @@ def _count_comment(
358
336
  counts.update(f"{prefix}{_normalized_ruff_selector(code, ruff_aliases)}" for code in match.group(1).split(","))
359
337
  for match in _SARJ_NOQA_RE.finditer(comment):
360
338
  counts.update(f"sarj-noqa:{code.strip().upper()}" for code in match.group(1).split(","))
339
+ _count_type_checker_comments(comment, counts)
340
+
341
+
342
+ def _count_type_checker_comments(comment: str, counts: Counter[str]) -> None:
361
343
  for match in _PYRIGHT_IGNORE_RE.finditer(comment):
362
344
  counts.update(f"pyright:{code.strip()}" for code in match.group(1).split(","))
363
345
  if re.search(r"#\s*pyright:\s*ignore\b(?!\s*\[)", comment):
@@ -370,6 +352,34 @@ def _count_comment(
370
352
  counts[_BARE_TYPE_IGNORE_KEY] += 1
371
353
 
372
354
 
355
+ def _count_file_comment(comment: str, counts: Counter[str], *, ruff_aliases: Mapping[str, str] | None) -> bool:
356
+ if file_ignore := _RUFF_FILE_IGNORE_RE.match(comment):
357
+ counts.update(
358
+ f"file-noqa:{_normalized_ruff_selector(code, ruff_aliases)}" for code in file_ignore.group(1).split(",")
359
+ )
360
+ return True
361
+ if file_noqa := _FILE_NOQA_RE.match(comment):
362
+ listed = file_noqa.group(1)
363
+ if listed:
364
+ counts.update(f"file-noqa:{_normalized_ruff_selector(code, ruff_aliases)}" for code in listed.split(","))
365
+ else:
366
+ counts[_BLANKET_KEY] += 1
367
+ return True
368
+ if _FLAKE8_FILE_NOQA_RE.match(comment):
369
+ counts[_FLAKE8_BLANKET_KEY] += 1
370
+ return True
371
+ if disabled := _RUFF_DISABLE_RE.match(comment):
372
+ counts.update(
373
+ f"ruff-range:{_normalized_ruff_selector(code, ruff_aliases)}" for code in disabled.group(1).split(",")
374
+ )
375
+ return True
376
+ if _FILE_PYRIGHT_RE.match(comment):
377
+ rules: list[str] = _FILE_PYRIGHT_RULE_RE.findall(comment)
378
+ counts.update(f"file-pyright:{rule}" for rule in rules)
379
+ return True
380
+ return False
381
+
382
+
373
383
  def _normalized_ruff_selector(selector: str, aliases: Mapping[str, str] | None) -> str:
374
384
  stripped = selector.strip()
375
385
  normalized = stripped.upper() if stripped.isupper() else stripped.lower()
@@ -184,6 +184,9 @@ class RuleDocumentation:
184
184
  if len(example_ids) != len(set(example_ids)):
185
185
  msg = "rule example IDs must be unique"
186
186
  raise ValueError(msg)
187
+ self._validate_public_scenarios()
188
+
189
+ def _validate_public_scenarios(self) -> None:
187
190
  public_scenarios = {example.scenario for example in self.examples if example.public}
188
191
  for scenario in public_scenarios:
189
192
  pair = tuple(example for example in self.examples if example.public and example.scenario == scenario)
@@ -457,32 +457,7 @@ def statement_comment_walls(
457
457
  statements = [item for item in value if _is_wall_statement(item)]
458
458
  if len(statements) < _WALL_MIN_STATEMENTS:
459
459
  continue
460
- attached: list[tuple[int, int, bool]] = []
461
- for index, statement in enumerate(statements):
462
- entry = comments.get(statement.lineno - 1)
463
- if entry is None or entry[0] != statement.col_offset:
464
- continue
465
- line = statement.lineno - 1
466
- segment = ast.get_source_segment(source, statement) or ""
467
- attached.append((index, line, _weak_walkthrough_comment(entry[1], segment)))
468
- clusters: list[list[tuple[int, int, bool]]] = []
469
- for item in attached:
470
- if clusters and item[0] <= clusters[-1][-1][0] + 2:
471
- clusters[-1].append(item)
472
- else:
473
- clusters.append([item])
474
- for cluster in clusters:
475
- span = cluster[-1][0] - cluster[0][0] + 1
476
- weak = [line for _index, line, is_weak in cluster if is_weak]
477
- if (
478
- span < _WALL_MIN_STATEMENTS
479
- or len(weak) < _WALL_MIN_COMMENTS
480
- or len(cluster) / span < _WALL_MIN_COMMENTED_RATIO
481
- or len(weak) / len(cluster) < _WALL_MIN_WEAK_RATIO
482
- ):
483
- continue
484
- leader = min(weak)
485
- walls[leader] = frozenset(weak)
460
+ _collect_statement_walls(statements, comments, source, walls)
486
461
  return walls
487
462
 
488
463
 
@@ -513,6 +488,32 @@ def _is_wall_statement(node: ast.stmt) -> bool:
513
488
  )
514
489
 
515
490
 
491
+ def _collect_statement_walls(
492
+ statements: list[ast.stmt], comments: dict[int, tuple[int, str]], source: str, walls: dict[int, frozenset[int]]
493
+ ) -> None:
494
+ attached: list[tuple[int, int, bool]] = []
495
+ for index, statement in enumerate(statements):
496
+ entry = comments.get(statement.lineno - 1)
497
+ if entry is None or entry[0] != statement.col_offset:
498
+ continue
499
+ line = statement.lineno - 1
500
+ segment = ast.get_source_segment(source, statement) or ""
501
+ attached.append((index, line, _weak_walkthrough_comment(entry[1], segment)))
502
+ clusters = _comment_clusters(attached)
503
+ for cluster in clusters:
504
+ span = cluster[-1][0] - cluster[0][0] + 1
505
+ weak = [line for _index, line, is_weak in cluster if is_weak]
506
+ if (
507
+ span < _WALL_MIN_STATEMENTS
508
+ or len(weak) < _WALL_MIN_COMMENTS
509
+ or len(cluster) / span < _WALL_MIN_COMMENTED_RATIO
510
+ or len(weak) / len(cluster) < _WALL_MIN_WEAK_RATIO
511
+ ):
512
+ continue
513
+ leader = min(weak)
514
+ walls[leader] = frozenset(weak)
515
+
516
+
516
517
  def _weak_walkthrough_comment(body: str, statement: str) -> bool:
517
518
  if (
518
519
  not body
@@ -533,3 +534,13 @@ def _weak_walkthrough_comment(body: str, statement: str) -> bool:
533
534
  matched = sum(1 for word in described if restates([word], known))
534
535
  novel = len(described) - matched
535
536
  return matched / len(described) >= _WALL_MIN_MATCHED_RATIO and novel <= _WALL_MAX_NOVEL_WORDS
537
+
538
+
539
+ def _comment_clusters(attached: list[tuple[int, int, bool]]) -> list[list[tuple[int, int, bool]]]:
540
+ clusters: list[list[tuple[int, int, bool]]] = []
541
+ for item in attached:
542
+ if clusters and item[0] <= clusters[-1][-1][0] + 2:
543
+ clusters[-1].append(item)
544
+ else:
545
+ clusters.append([item])
546
+ return clusters