sarj-python-lint 0.69.0__tar.gz → 0.69.1__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.69.0 → sarj_python_lint-0.69.1}/PKG-INFO +1 -1
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/pyproject.toml +1 -1
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +109 -26
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/.gitignore +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/LICENSE +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/README.md +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/created_at_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/defect_xfail_requires_strict.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/duplicate_test_body.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/duplicated_override_docstring.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/fastapi_openapi_contract.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_long_comment.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/source_coupled_test.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 0.69.
|
|
3
|
+
Version: 0.69.1
|
|
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/
|
|
@@ -18,6 +18,7 @@ from sarj_python_lint.rule_base import (
|
|
|
18
18
|
RuleCategory,
|
|
19
19
|
RuleDocumentation,
|
|
20
20
|
RuleExample,
|
|
21
|
+
Severity,
|
|
21
22
|
parse_or_none,
|
|
22
23
|
)
|
|
23
24
|
from sarj_python_lint.rules._paths import is_generated, is_test_path
|
|
@@ -29,7 +30,8 @@ if TYPE_CHECKING:
|
|
|
29
30
|
|
|
30
31
|
_GENERIC_EXCEPTIONS = frozenset({"Exception", "BaseException"})
|
|
31
32
|
_MIN_SENTINEL_COUNT = 2
|
|
32
|
-
_MIN_TYPE_DISPATCH_BRANCHES =
|
|
33
|
+
_MIN_TYPE_DISPATCH_BRANCHES = 2
|
|
34
|
+
_ERROR_TYPE_DISPATCH_BRANCHES = 3
|
|
33
35
|
_ISINSTANCE_ARG_COUNT = 2
|
|
34
36
|
_PAIR_COUNT = 2
|
|
35
37
|
|
|
@@ -238,7 +240,7 @@ def _matchable_class_reference(
|
|
|
238
240
|
case ast.Name(id=name):
|
|
239
241
|
if name in runtime_tuple_aliases:
|
|
240
242
|
return False
|
|
241
|
-
return name in _MATCHABLE_BUILTIN_TYPES or (name
|
|
243
|
+
return name in _MATCHABLE_BUILTIN_TYPES or _looks_like_class_name(name)
|
|
242
244
|
case ast.Attribute(value=value, attr=attribute):
|
|
243
245
|
lowercase_ast_type = (
|
|
244
246
|
isinstance(value, ast.Name)
|
|
@@ -246,15 +248,19 @@ def _matchable_class_reference(
|
|
|
246
248
|
and attribute in _MATCHABLE_LOWERCASE_AST_TYPES
|
|
247
249
|
)
|
|
248
250
|
matchable_leaf = (
|
|
249
|
-
attribute in _MATCHABLE_BUILTIN_TYPES
|
|
250
|
-
or (attribute[:1].isupper() and not attribute.isupper())
|
|
251
|
-
or lowercase_ast_type
|
|
251
|
+
attribute in _MATCHABLE_BUILTIN_TYPES or _looks_like_class_name(attribute) or lowercase_ast_type
|
|
252
252
|
)
|
|
253
253
|
return matchable_leaf and _is_dotted_name(value)
|
|
254
254
|
case _:
|
|
255
255
|
return False
|
|
256
256
|
|
|
257
257
|
|
|
258
|
+
def _looks_like_class_name(name: str) -> bool:
|
|
259
|
+
"""Recognize ordinary and private CamelCase runtime class references."""
|
|
260
|
+
unprefixed = name.lstrip("_")
|
|
261
|
+
return bool(unprefixed) and unprefixed[:1].isupper() and not unprefixed.isupper()
|
|
262
|
+
|
|
263
|
+
|
|
258
264
|
def _is_dotted_name(node: ast.expr) -> bool:
|
|
259
265
|
"""Report whether `node` is the prefix of a pattern-compatible dotted name."""
|
|
260
266
|
if isinstance(node, ast.Name):
|
|
@@ -279,30 +285,74 @@ def _is_matchable_raise_guard(test: ast.expr, runtime_tuple_aliases: frozenset[s
|
|
|
279
285
|
)
|
|
280
286
|
|
|
281
287
|
|
|
282
|
-
def _isinstance_ladder(
|
|
288
|
+
def _isinstance_ladder(
|
|
289
|
+
node: ast.If,
|
|
290
|
+
runtime_tuple_aliases: frozenset[str],
|
|
291
|
+
ast_module_names: frozenset[str],
|
|
292
|
+
) -> tuple[str, int] | None:
|
|
283
293
|
"""Recognize a complete if/elif chain dispatching one name by runtime type."""
|
|
294
|
+
branches = _if_ladder(node)
|
|
295
|
+
branch_count = len(branches)
|
|
296
|
+
if branch_count < _MIN_TYPE_DISPATCH_BRANCHES:
|
|
297
|
+
return None
|
|
298
|
+
|
|
284
299
|
subject: str | None = None
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
300
|
+
for branch in branches:
|
|
301
|
+
# Guarded ladders are part of the newly calibrated two-arm advisory.
|
|
302
|
+
# Preserve SARJ080's established 3+ ladder boundary to avoid promoting
|
|
303
|
+
# an uncalibrated population directly to blocking diagnostics.
|
|
304
|
+
branch_subject = (
|
|
305
|
+
_matchable_isinstance_dispatch(branch.test, runtime_tuple_aliases, frozenset())
|
|
306
|
+
if branch_count < _ERROR_TYPE_DISPATCH_BRANCHES
|
|
307
|
+
else _matchable_isinstance_test(branch.test, runtime_tuple_aliases)
|
|
308
|
+
)
|
|
289
309
|
if branch_subject is None or (subject is not None and branch_subject != subject):
|
|
290
310
|
return None
|
|
291
311
|
subject = branch_subject
|
|
292
|
-
branch_count += 1
|
|
293
312
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
break
|
|
300
|
-
|
|
301
|
-
if branch_count < _MIN_TYPE_DISPATCH_BRANCHES:
|
|
313
|
+
if branch_count < _ERROR_TYPE_DISPATCH_BRANCHES and any(
|
|
314
|
+
_dispatch_uses_stdlib_ast(branch.test, ast_module_names) for branch in branches
|
|
315
|
+
):
|
|
316
|
+
return None
|
|
317
|
+
if subject is None:
|
|
302
318
|
return None
|
|
303
319
|
return subject, branch_count
|
|
304
320
|
|
|
305
321
|
|
|
322
|
+
def _if_ladder(node: ast.If) -> tuple[ast.If, ...]:
|
|
323
|
+
"""Return the complete maximal if/elif chain rooted at ``node``."""
|
|
324
|
+
branches = [node]
|
|
325
|
+
current = node
|
|
326
|
+
while len(current.orelse) == 1 and isinstance(current.orelse[0], ast.If):
|
|
327
|
+
current = current.orelse[0]
|
|
328
|
+
branches.append(current)
|
|
329
|
+
return tuple(branches)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _dispatch_uses_stdlib_ast(test: ast.expr, ast_module_names: frozenset[str]) -> bool:
|
|
333
|
+
"""Report whether a dispatch arm directly names a proven stdlib ``ast`` class."""
|
|
334
|
+
call = _leading_isinstance(test)
|
|
335
|
+
if call is None or len(call.args) != _ISINSTANCE_ARG_COUNT:
|
|
336
|
+
return False
|
|
337
|
+
return any(
|
|
338
|
+
isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name) and node.value.id in ast_module_names
|
|
339
|
+
for node in ast.walk(call.args[1])
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _leading_isinstance(test: ast.expr) -> ast.Call | None:
|
|
344
|
+
"""Return the leading isinstance call from a plain or guarded dispatch arm."""
|
|
345
|
+
candidate = test.values[0] if isinstance(test, ast.BoolOp) and isinstance(test.op, ast.And) else test
|
|
346
|
+
if isinstance(candidate, ast.Call) and isinstance(candidate.func, ast.Name) and candidate.func.id == "isinstance":
|
|
347
|
+
return candidate
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _type_dispatch_severity(branch_count: int) -> Severity:
|
|
352
|
+
"""Keep newly expanded two-arm findings advisory while 3+ remains blocking."""
|
|
353
|
+
return Severity.WARNING if branch_count < _ERROR_TYPE_DISPATCH_BRANCHES else Severity.ERROR
|
|
354
|
+
|
|
355
|
+
|
|
306
356
|
def _sequential_isinstance_dispatches(
|
|
307
357
|
tree: ast.Module, path: Path, code: str, runtime_tuple_aliases: frozenset[str]
|
|
308
358
|
) -> list[Diagnostic]:
|
|
@@ -336,15 +386,28 @@ def _sequential_isinstance_dispatches(
|
|
|
336
386
|
break
|
|
337
387
|
end += 1
|
|
338
388
|
branch_count = end - index
|
|
389
|
+
adjacent_if = (index > 0 and isinstance(statements[index - 1], ast.If)) or (
|
|
390
|
+
end < len(statements) and isinstance(statements[end], ast.If)
|
|
391
|
+
)
|
|
339
392
|
guards = [_passthrough_guard_var(statement) for statement in statements[index:end]]
|
|
340
393
|
owned_by_sentinel_arm = guards[:_MIN_SENTINEL_COUNT] == [subject] * _MIN_SENTINEL_COUNT
|
|
341
|
-
|
|
394
|
+
exact_two_ast_dispatch = branch_count < _ERROR_TYPE_DISPATCH_BRANCHES and any(
|
|
395
|
+
isinstance(statement, ast.If) and _dispatch_uses_stdlib_ast(statement.test, ast_module_names)
|
|
396
|
+
for statement in statements[index:end]
|
|
397
|
+
)
|
|
398
|
+
if (
|
|
399
|
+
branch_count >= _MIN_TYPE_DISPATCH_BRANCHES
|
|
400
|
+
and not adjacent_if
|
|
401
|
+
and not owned_by_sentinel_arm
|
|
402
|
+
and not exact_two_ast_dispatch
|
|
403
|
+
):
|
|
342
404
|
findings.append(
|
|
343
405
|
Diagnostic(
|
|
344
406
|
path=path,
|
|
345
407
|
line=first.lineno,
|
|
346
408
|
col=first.col_offset + 1,
|
|
347
409
|
code=code,
|
|
410
|
+
severity=_type_dispatch_severity(branch_count),
|
|
348
411
|
message=(
|
|
349
412
|
f"{branch_count}-branch terminating isinstance dispatch on '{subject}' — use "
|
|
350
413
|
"match/case class patterns so the exclusive type dispatch is explicit."
|
|
@@ -396,16 +459,24 @@ def _matchable_isinstance_dispatch(
|
|
|
396
459
|
) -> str | None:
|
|
397
460
|
"""Extract a type-dispatch subject, preserving any trailing ``and`` terms as a case guard."""
|
|
398
461
|
match test:
|
|
399
|
-
case ast.BoolOp(op=ast.And(), values=[leading_isinstance, *
|
|
400
|
-
|
|
462
|
+
case ast.BoolOp(op=ast.And(), values=[leading_isinstance, *guard_terms]):
|
|
463
|
+
subject = _matchable_isinstance_test(
|
|
401
464
|
leading_isinstance,
|
|
402
465
|
runtime_tuple_aliases,
|
|
403
466
|
ast_module_names=ast_module_names,
|
|
404
467
|
)
|
|
468
|
+
if subject is None or any(_rebinds_name(term, subject) for term in guard_terms):
|
|
469
|
+
return None
|
|
470
|
+
return subject
|
|
405
471
|
case _:
|
|
406
472
|
return _matchable_isinstance_test(test, runtime_tuple_aliases)
|
|
407
473
|
|
|
408
474
|
|
|
475
|
+
def _rebinds_name(node: ast.AST, name: str) -> bool:
|
|
476
|
+
"""Reject guards that change the dispatch subject after ``match`` would snapshot it."""
|
|
477
|
+
return any(isinstance(candidate, ast.NamedExpr) and candidate.target.id == name for candidate in ast.walk(node))
|
|
478
|
+
|
|
479
|
+
|
|
409
480
|
def _body_terminates(body: list[ast.stmt]) -> bool:
|
|
410
481
|
"""Prove a branch cannot fall through to the following sibling statement."""
|
|
411
482
|
statements = [statement for statement in body if not _is_docstring(statement)]
|
|
@@ -419,6 +490,13 @@ def _body_terminates(body: list[ast.stmt]) -> bool:
|
|
|
419
490
|
return bool(orelse) and _body_terminates(body) and _body_terminates(orelse)
|
|
420
491
|
case ast.With() | ast.AsyncWith():
|
|
421
492
|
return _body_terminates(last.body)
|
|
493
|
+
case ast.Try() | ast.TryStar():
|
|
494
|
+
if last.finalbody and _body_terminates(last.finalbody):
|
|
495
|
+
return True
|
|
496
|
+
normal_path_terminates = _body_terminates(last.body) or (
|
|
497
|
+
bool(last.orelse) and _body_terminates(last.orelse)
|
|
498
|
+
)
|
|
499
|
+
return normal_path_terminates and all(_body_terminates(handler.body) for handler in last.handlers)
|
|
422
500
|
case _:
|
|
423
501
|
return False
|
|
424
502
|
|
|
@@ -683,12 +761,14 @@ class _TypeDispatchVisitor(ast.NodeVisitor):
|
|
|
683
761
|
report_control_flow_raise: bool,
|
|
684
762
|
report_isinstance_ladder: bool,
|
|
685
763
|
runtime_tuple_aliases: frozenset[str],
|
|
764
|
+
ast_module_names: frozenset[str],
|
|
686
765
|
) -> None:
|
|
687
766
|
self.path: Path = path
|
|
688
767
|
self.code: str = code
|
|
689
768
|
self.report_control_flow_raise: bool = report_control_flow_raise
|
|
690
769
|
self.report_isinstance_ladder: bool = report_isinstance_ladder
|
|
691
770
|
self.runtime_tuple_aliases: frozenset[str] = runtime_tuple_aliases
|
|
771
|
+
self.ast_module_names: frozenset[str] = ast_module_names
|
|
692
772
|
self.diags: list[Diagnostic] = []
|
|
693
773
|
self.try_stack: list[ast.Try | ast.TryStar] = []
|
|
694
774
|
self.if_stack: list[ast.If] = []
|
|
@@ -700,7 +780,7 @@ class _TypeDispatchVisitor(ast.NodeVisitor):
|
|
|
700
780
|
while len(continuation.orelse) == 1 and isinstance(continuation.orelse[0], ast.If):
|
|
701
781
|
continuation = continuation.orelse[0]
|
|
702
782
|
self.ladder_continuations.add(id(continuation))
|
|
703
|
-
ladder = _isinstance_ladder(node, self.runtime_tuple_aliases)
|
|
783
|
+
ladder = _isinstance_ladder(node, self.runtime_tuple_aliases, self.ast_module_names)
|
|
704
784
|
if ladder is not None:
|
|
705
785
|
subject, branch_count = ladder
|
|
706
786
|
self.diags.append(
|
|
@@ -709,6 +789,7 @@ class _TypeDispatchVisitor(ast.NodeVisitor):
|
|
|
709
789
|
line=node.lineno,
|
|
710
790
|
col=node.col_offset + 1,
|
|
711
791
|
code=self.code,
|
|
792
|
+
severity=_type_dispatch_severity(branch_count),
|
|
712
793
|
message=(
|
|
713
794
|
f"{branch_count}-branch isinstance dispatch on '{subject}' — use match/case "
|
|
714
795
|
"class patterns (combining tuple members with `|`) so the type dispatch is explicit."
|
|
@@ -815,14 +896,14 @@ class PreferMatchTypeDispatch(Rule):
|
|
|
815
896
|
id: str = "prefer-match-type-dispatch"
|
|
816
897
|
code: str = "SARJ080"
|
|
817
898
|
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
818
|
-
summary="Use `match` for explicit runtime type dispatch instead of
|
|
899
|
+
summary="Use `match` for explicit runtime type dispatch instead of repeated `isinstance` branches or parser machinery.",
|
|
819
900
|
rationale="Pattern matching makes type cases and sentinel cases explicit without control-flow exceptions or repeated dispatch checks.",
|
|
820
901
|
remediation="Replace the detected guard or exception-driven dispatch with `match` arms for each supported value shape.",
|
|
821
902
|
category=RuleCategory.MAINTAINABILITY,
|
|
822
903
|
autofix=AutofixPolicy.NONE,
|
|
823
904
|
limitations=(
|
|
824
|
-
"
|
|
825
|
-
"Generated files, runtime tuple aliases, idiomatic None/Unset prologues, and code that shadows `isinstance` are excluded; test files omit the control-flow-raise check.",
|
|
905
|
+
"Safe two-branch `isinstance` dispatch is advisory; dispatch with three or more branches and the rule's established parser shapes remain blocking.",
|
|
906
|
+
"Generated files, runtime tuple aliases, two-arm stdlib AST visitors, subject-rebinding guards, idiomatic None/Unset prologues, and code that shadows `isinstance` are excluded; test files omit the control-flow-raise check.",
|
|
826
907
|
),
|
|
827
908
|
examples=(
|
|
828
909
|
RuleExample(
|
|
@@ -881,6 +962,7 @@ class PreferMatchTypeDispatch(Rule):
|
|
|
881
962
|
if tree is None:
|
|
882
963
|
return []
|
|
883
964
|
runtime_tuple_aliases = _runtime_tuple_aliases(tree)
|
|
965
|
+
ast_module_names = _stdlib_ast_module_names(tree)
|
|
884
966
|
|
|
885
967
|
visitor = _TypeDispatchVisitor(
|
|
886
968
|
path,
|
|
@@ -888,6 +970,7 @@ class PreferMatchTypeDispatch(Rule):
|
|
|
888
970
|
report_control_flow_raise=not is_test_path(path),
|
|
889
971
|
report_isinstance_ladder=not _shadows_isinstance(tree),
|
|
890
972
|
runtime_tuple_aliases=runtime_tuple_aliases,
|
|
973
|
+
ast_module_names=ast_module_names,
|
|
891
974
|
)
|
|
892
975
|
visitor.visit(tree)
|
|
893
976
|
if not _shadows_isinstance(tree):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/_prose_budget.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/_test_assertions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/mock_without_spec.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_comment_cruft.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_long_comment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_secret_in_log.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_select_star.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/no_stdlib_logging.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/over_mocked_test.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/prefer_class_row.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/prefer_or_pattern.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/prefer_str_enum.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.69.0 → sarj_python_lint-0.69.1}/src/sarj_python_lint/rules/unused_mock_setup.py
RENAMED
|
File without changes
|