sarj-python-lint 0.26.0__tar.gz → 0.28.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/PKG-INFO +1 -1
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/pyproject.toml +1 -1
- sarj_python_lint-0.28.0/src/sarj_python_lint/rules/_ast_index.py +180 -0
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/_comments.py +35 -5
- sarj_python_lint-0.28.0/src/sarj_python_lint/rules/_logging.py +54 -0
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/_pytest.py +3 -1
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/_registry.py +0 -6
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/_suppression_comments.py +22 -31
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/conditional_assertion_in_test.py +15 -16
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/duplicate_test_body.py +2 -1
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +4 -3
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/inefficient_string_concat_in_loop.py +5 -4
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/interaction_only_test.py +6 -5
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +2 -3
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/kwonly_same_type_params.py +19 -18
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/mock_without_spec.py +7 -8
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +3 -4
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_comment_cruft.py +22 -6
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +3 -4
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_fat_try_blocks.py +3 -4
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +3 -3
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +3 -2
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_fstring_in_log.py +5 -4
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +32 -2
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_implicit_attribute_access.py +47 -27
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +3 -4
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_offset_pagination.py +3 -4
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +5 -4
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_patching_system_under_test.py +70 -3
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +3 -4
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_raw_sql_in_tests.py +2 -3
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +4 -3
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_secret_in_log.py +2 -3
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_select_star.py +3 -4
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +14 -10
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_sequential_await.py +74 -51
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_sleep_in_test_body.py +2 -1
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +32 -17
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_tautological_expect.py +37 -5
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_unreachable_after_terminal.py +15 -1
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/over_mocked_test.py +4 -3
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/parametrize_case_needs_id.py +2 -7
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/prefer_class_row.py +2 -3
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +36 -26
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +3 -2
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/prefer_library_fake.py +4 -3
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +7 -10
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/prefer_match_pattern_destructuring.py +3 -2
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +35 -109
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +3 -4
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +4 -5
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +4 -3
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +3 -3
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/prefer_real_store_in_tests.py +5 -4
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +1 -1
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/prefer_str_enum.py +10 -13
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +3 -3
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +4 -3
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +8 -8
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +3 -2
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +4 -8
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +4 -5
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/redundant_docstring.py +2 -1
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/require_port_for_service.py +21 -1
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/stepdown.py +2 -1
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +3 -4
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/tautological_mock_assertion.py +183 -10
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/test_loops_over_literal_cases.py +3 -2
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +48 -172
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/unused_mock_setup.py +8 -9
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/xfail_requires_strict.py +3 -4
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/zero_assertion_test.py +5 -5
- sarj_python_lint-0.26.0/src/sarj_python_lint/rules/_logging.py +0 -39
- sarj_python_lint-0.26.0/src/sarj_python_lint/rules/prefer_pattern_matching.py +0 -340
- sarj_python_lint-0.26.0/src/sarj_python_lint/rules/primary_export_file_name.py +0 -149
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/.gitignore +0 -0
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/README.md +0 -0
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -0
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/single_public_export.py +0 -0
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/sleep_with_computed_arg_in_test.py +0 -0
- {sarj_python_lint-0.26.0 → sarj_python_lint-0.28.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.28.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
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""One tree traversal per file, shared by every rule that needs node lookups.
|
|
2
|
+
|
|
3
|
+
Profiling the whole registry over a 1,920-line module showed `ast.walk` and its
|
|
4
|
+
helpers (`iter_child_nodes` / `iter_fields`) accounting for ~60% of all rule
|
|
5
|
+
time. The cause is structural rather than any one rule's fault: the dominant
|
|
6
|
+
shape of a rule is
|
|
7
|
+
|
|
8
|
+
for node in ast.walk(tree):
|
|
9
|
+
if isinstance(node, ast.Something):
|
|
10
|
+
...
|
|
11
|
+
|
|
12
|
+
so a file with N nodes and R rules asking that question pays R full traversals,
|
|
13
|
+
each one re-deriving the same partition of the tree by node type.
|
|
14
|
+
|
|
15
|
+
`nodes()` derives that partition **once per file** and answers every subsequent
|
|
16
|
+
type query from it. The index is memoized in a single slot keyed on tree
|
|
17
|
+
identity, mirroring `parse_or_none`: the CLI iterates files on the outer loop
|
|
18
|
+
and rules on the inner one, so one slot is all that is ever live. The slot holds
|
|
19
|
+
a strong reference to the tree, so a recycled `id()` can never alias a stale
|
|
20
|
+
index.
|
|
21
|
+
|
|
22
|
+
Two invariants make the substitution finding-preserving, not merely faster:
|
|
23
|
+
|
|
24
|
+
* **Order.** The index is built by breadth-first traversal, the same order
|
|
25
|
+
`ast.walk` yields, and every query preserves it. `nodes(tree, T)` is therefore
|
|
26
|
+
elementwise equal to `[n for n in ast.walk(tree) if isinstance(n, T)]`, so a
|
|
27
|
+
rule that takes the *first* match, or reports in discovery order, is
|
|
28
|
+
unaffected.
|
|
29
|
+
* **Subclasses.** Queries match by `isinstance`, not by exact type: the buckets
|
|
30
|
+
are keyed on exact class, and a query resolves to the set of present classes
|
|
31
|
+
that are subclasses of the requested types. Asking for `ast.stmt` still
|
|
32
|
+
returns every statement.
|
|
33
|
+
|
|
34
|
+
`walk()` is the same breadth-first order for the cases the index cannot serve —
|
|
35
|
+
traversing a *subtree* rather than the module, where containment is not
|
|
36
|
+
something a whole-module partition can answer.
|
|
37
|
+
|
|
38
|
+
No rule may mutate the tree. That was already required — every rule shares one
|
|
39
|
+
memoized parse, and `duplicate_test_body` deep-copies before normalizing — and
|
|
40
|
+
the index inherits the requirement, since it holds references into the tree it
|
|
41
|
+
was built from.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import ast
|
|
47
|
+
from typing import TYPE_CHECKING, final
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if TYPE_CHECKING:
|
|
51
|
+
from collections.abc import Iterator
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
_AST = ast.AST
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def children(node: ast.AST) -> list[ast.AST]:
|
|
58
|
+
"""`node`'s direct children, in the same order as `ast.iter_child_nodes`.
|
|
59
|
+
|
|
60
|
+
A list rather than a generator: every caller either iterates it once or
|
|
61
|
+
extends a stack with it, and the hand-rolled walkers in this package call
|
|
62
|
+
this once per node, where the generator's setup cost dominates the work.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
The child nodes, in field-declaration order.
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
out: list[ast.AST] = []
|
|
69
|
+
for name in node._fields:
|
|
70
|
+
value: object = getattr(node, name, None)
|
|
71
|
+
if isinstance(value, list):
|
|
72
|
+
out += [item for item in value if isinstance(item, _AST)] # pyright: ignore[reportUnknownVariableType] — element narrowed by isinstance
|
|
73
|
+
elif isinstance(value, _AST):
|
|
74
|
+
out.append(value)
|
|
75
|
+
return out
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def walk(node: ast.AST) -> Iterator[ast.AST]:
|
|
79
|
+
"""Yield `node` and every descendant, breadth-first.
|
|
80
|
+
|
|
81
|
+
Identical in order to `ast.walk`, but reads each node's fields directly
|
|
82
|
+
instead of routing every node through two intermediate generators.
|
|
83
|
+
|
|
84
|
+
Yields:
|
|
85
|
+
`node` first, then its descendants breadth-first.
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
queue: list[ast.AST] = [node]
|
|
89
|
+
i = 0
|
|
90
|
+
while i < len(queue):
|
|
91
|
+
current = queue[i]
|
|
92
|
+
i += 1
|
|
93
|
+
for name in current._fields:
|
|
94
|
+
value: object = getattr(current, name, None)
|
|
95
|
+
if isinstance(value, list):
|
|
96
|
+
queue += [item for item in value if isinstance(item, _AST)] # pyright: ignore[reportUnknownVariableType] — element narrowed by isinstance
|
|
97
|
+
elif isinstance(value, _AST):
|
|
98
|
+
queue.append(value)
|
|
99
|
+
yield current
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@final
|
|
103
|
+
class _NodeIndex:
|
|
104
|
+
"""A module's nodes partitioned by exact class, in breadth-first order."""
|
|
105
|
+
|
|
106
|
+
__slots__ = ("_buckets", "_flat", "_queries")
|
|
107
|
+
|
|
108
|
+
def __init__(self, tree: ast.AST) -> None:
|
|
109
|
+
buckets: dict[type[ast.AST], list[ast.AST]] = {}
|
|
110
|
+
flat: list[ast.AST] = [tree]
|
|
111
|
+
i = 0
|
|
112
|
+
while i < len(flat):
|
|
113
|
+
current = flat[i]
|
|
114
|
+
i += 1
|
|
115
|
+
cls = current.__class__
|
|
116
|
+
bucket = buckets.get(cls)
|
|
117
|
+
if bucket is None:
|
|
118
|
+
buckets[cls] = [current]
|
|
119
|
+
else:
|
|
120
|
+
bucket.append(current)
|
|
121
|
+
for name in cls._fields:
|
|
122
|
+
value: object = getattr(current, name, None)
|
|
123
|
+
if isinstance(value, list):
|
|
124
|
+
flat += [item for item in value if isinstance(item, _AST)] # pyright: ignore[reportUnknownVariableType] — element narrowed by isinstance
|
|
125
|
+
elif isinstance(value, _AST):
|
|
126
|
+
flat.append(value)
|
|
127
|
+
self._buckets: dict[type[ast.AST], list[ast.AST]] = buckets
|
|
128
|
+
self._flat: list[ast.AST] = flat
|
|
129
|
+
self._queries: dict[tuple[type[ast.AST], ...], list[ast.AST]] = {}
|
|
130
|
+
|
|
131
|
+
def query(self, types: tuple[type[ast.AST], ...]) -> list[ast.AST]:
|
|
132
|
+
"""Return every node matching `isinstance(node, types)`, breadth-first.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
The match list, cached per `types` and shared — treat as read-only.
|
|
136
|
+
|
|
137
|
+
"""
|
|
138
|
+
hit = self._queries.get(types)
|
|
139
|
+
if hit is not None:
|
|
140
|
+
return hit
|
|
141
|
+
matched = frozenset(cls for cls in self._buckets if issubclass(cls, types))
|
|
142
|
+
if len(matched) == 1:
|
|
143
|
+
# The single bucket is already the answer, in breadth-first order.
|
|
144
|
+
result = self._buckets[next(iter(matched))]
|
|
145
|
+
elif len(matched) == len(self._buckets):
|
|
146
|
+
# Every class present matches — e.g. `nodes(tree, ast.AST)`, asked by
|
|
147
|
+
# a rule whose loop body inspects every node rather than a few types.
|
|
148
|
+
result = self._flat
|
|
149
|
+
elif matched:
|
|
150
|
+
result = [node for node in self._flat if node.__class__ in matched]
|
|
151
|
+
else:
|
|
152
|
+
result = []
|
|
153
|
+
self._queries[types] = result
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
_last_index: tuple[ast.AST, _NodeIndex] | None = None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def nodes[NodeT: ast.AST](tree: ast.AST, *types: type[NodeT]) -> list[NodeT]:
|
|
161
|
+
"""Every node of `tree` matching `types`, in `ast.walk` order.
|
|
162
|
+
|
|
163
|
+
A drop-in, memoized replacement for the ubiquitous
|
|
164
|
+
`[n for n in ast.walk(tree) if isinstance(n, types)]`. Pass the *module*
|
|
165
|
+
tree: the index is per-file, so calling this with a subtree builds a fresh
|
|
166
|
+
index and evicts the module's. Use `walk` for subtrees.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
The matching nodes, breadth-first. A fresh list per call; the partition
|
|
170
|
+
it is drawn from is what gets cached.
|
|
171
|
+
|
|
172
|
+
"""
|
|
173
|
+
global _last_index # ruff: ignore[global-statement] — single-slot memo, mirroring `parse_or_none`
|
|
174
|
+
if _last_index is None or _last_index[0] is not tree:
|
|
175
|
+
_last_index = (tree, _NodeIndex(tree))
|
|
176
|
+
# The `isinstance` pass is what narrows `list[ast.AST]` to `list[NodeT]`
|
|
177
|
+
# honestly, rather than asserting it with a `cast` the house style bans. It
|
|
178
|
+
# runs over the matches only — never the tree — and every element passes by
|
|
179
|
+
# construction, so it costs one type check per node actually returned.
|
|
180
|
+
return [node for node in _last_index[1].query(types) if isinstance(node, types)]
|
|
@@ -397,7 +397,12 @@ def restates(comment_tokens: Sequence[str], code: Iterable[str]) -> bool:
|
|
|
397
397
|
_LAYOUT_TOKENS = frozenset({tokenize.NL, tokenize.NEWLINE, tokenize.INDENT, tokenize.DEDENT})
|
|
398
398
|
_NON_CODE_TOKENS = _LAYOUT_TOKENS | frozenset({tokenize.COMMENT, tokenize.ENCODING, tokenize.ENDMARKER})
|
|
399
399
|
|
|
400
|
-
|
|
400
|
+
# `(line, col0, body, standalone)` for every comment, in source order. This is
|
|
401
|
+
# what `_suppression_comments` needs, and it is a by-product of the pass below
|
|
402
|
+
# rather than a reason to run a second one — see `all_comments`.
|
|
403
|
+
_Ordered = list[tuple[int, int, str, bool]]
|
|
404
|
+
|
|
405
|
+
_Scan = tuple[list[tuple[int, int, str]], list[tuple[int, int, str]], set[int], int, _Ordered]
|
|
401
406
|
|
|
402
407
|
_last_scan: tuple[str, _Scan] | None = None
|
|
403
408
|
|
|
@@ -405,6 +410,7 @@ _last_scan: tuple[str, _Scan] | None = None
|
|
|
405
410
|
def _scan(source: str) -> _Scan:
|
|
406
411
|
standalone: list[tuple[int, int, str]] = []
|
|
407
412
|
trailing: list[tuple[int, int, str]] = []
|
|
413
|
+
ordered: _Ordered = []
|
|
408
414
|
nested: set[int] = set()
|
|
409
415
|
first_code_line = 1 << 30
|
|
410
416
|
prev_end_row = 0
|
|
@@ -412,8 +418,11 @@ def _scan(source: str) -> _Scan:
|
|
|
412
418
|
readline = io.StringIO(source).readline
|
|
413
419
|
for tok in tokenize.generate_tokens(readline):
|
|
414
420
|
if tok.type == tokenize.COMMENT:
|
|
415
|
-
|
|
416
|
-
|
|
421
|
+
body = tok.string.lstrip("#").strip()
|
|
422
|
+
entry = (tok.start[0], tok.start[1], body)
|
|
423
|
+
is_standalone = tok.start[0] != prev_end_row
|
|
424
|
+
(standalone if is_standalone else trailing).append(entry)
|
|
425
|
+
ordered.append((tok.start[0], tok.start[1], body, is_standalone))
|
|
417
426
|
if depth > 0:
|
|
418
427
|
nested.add(tok.start[0])
|
|
419
428
|
elif tok.type == tokenize.OP:
|
|
@@ -425,7 +434,28 @@ def _scan(source: str) -> _Scan:
|
|
|
425
434
|
prev_end_row = tok.end[0]
|
|
426
435
|
if tok.type not in _NON_CODE_TOKENS:
|
|
427
436
|
first_code_line = min(first_code_line, tok.start[0])
|
|
428
|
-
return standalone, trailing, nested, first_code_line
|
|
437
|
+
return standalone, trailing, nested, first_code_line, ordered
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def all_comments(source: str) -> tuple[_Ordered, int]:
|
|
441
|
+
"""Return every comment as `(line, col0, body, standalone)`, plus the first code line.
|
|
442
|
+
|
|
443
|
+
Exists so the suppression rules (SARJ038/054) can share this module's
|
|
444
|
+
tokenize pass instead of running a second one. Both scanners computed the
|
|
445
|
+
same three facts — comment text, whether it stands alone on its line, and
|
|
446
|
+
where the first real code token is — from identical token-class sets, so the
|
|
447
|
+
second pass was pure duplicated work: SARJ038 alone spent ~4% of total rule
|
|
448
|
+
time on it.
|
|
449
|
+
|
|
450
|
+
`col0` is 0-based, matching this module's other accessors; the suppression
|
|
451
|
+
layer adds one for its 1-based `Comment.col`.
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
The ordered comments and the first code line's row.
|
|
455
|
+
|
|
456
|
+
"""
|
|
457
|
+
_, _, _, first_code_line, ordered = _scan_memo(source)
|
|
458
|
+
return ordered, first_code_line
|
|
429
459
|
|
|
430
460
|
|
|
431
461
|
def _scan_memo(source: str) -> _Scan:
|
|
@@ -482,7 +512,7 @@ def standalone_comments(source: str) -> tuple[list[tuple[int, int, str]], int]:
|
|
|
482
512
|
The standalone comments and the first code line's row.
|
|
483
513
|
|
|
484
514
|
"""
|
|
485
|
-
standalone, _, _, first_code_line = _scan_memo(source)
|
|
515
|
+
standalone, _, _, first_code_line, _ = _scan_memo(source)
|
|
486
516
|
return standalone, first_code_line
|
|
487
517
|
|
|
488
518
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Shared logging-receiver detection for SARJ012/SARJ017.
|
|
2
|
+
|
|
3
|
+
A single resolver for "does this receiver expression evaluate to a logger?",
|
|
4
|
+
used by both the secret-in-log and f-string-in-log rules so they recognise the
|
|
5
|
+
same factory/builder forms.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
_LOGGER_NAMES = frozenset({"logger", "log", "logging", "loguru", "_logger", "_log"})
|
|
14
|
+
|
|
15
|
+
_LOGGER_FACTORIES = frozenset({"getlogger", "get_logger"})
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def is_logger_expr(expr: ast.expr) -> bool:
|
|
19
|
+
"""Report whether `expr` evaluates to a logger.
|
|
20
|
+
|
|
21
|
+
Resolves the whole receiver chain so adapter/builder/factory calls are
|
|
22
|
+
caught: `logger.bind(...).info(...)`, `logger.opt(lazy=True).debug(...)`,
|
|
23
|
+
`logging.getLogger(__name__).info(...)`, `self.logger.error(...)`, and the
|
|
24
|
+
bare-name factory `get_logger().info(...)`.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
True when `expr` resolves to a logger receiver.
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
if isinstance(expr, ast.Name):
|
|
31
|
+
return expr.id.lower() in _LOGGER_NAMES
|
|
32
|
+
if isinstance(expr, ast.Attribute):
|
|
33
|
+
if expr.attr.lower() in _LOGGER_NAMES or expr.attr.lower() in _LOGGER_FACTORIES:
|
|
34
|
+
return True
|
|
35
|
+
return is_logger_expr(expr.value)
|
|
36
|
+
if isinstance(expr, ast.Call):
|
|
37
|
+
# A factory names a logger only when it is *called*: `get_logger()` is a
|
|
38
|
+
# logger, the bare name `get_logger` is a function — which is why the
|
|
39
|
+
# factories live in their own set and not in `_LOGGER_NAMES`. Both the
|
|
40
|
+
# dotted spelling (`structlog.get_logger()`, `logging.getLogger()`) and
|
|
41
|
+
# the bare one (`from structlog import get_logger`, then `get_logger()`)
|
|
42
|
+
# have to be recognised right here: a bare callee is an `ast.Name`, and
|
|
43
|
+
# recursing on it lands in the `_LOGGER_NAMES` branch, which by that
|
|
44
|
+
# design does not carry the factory names. Omitting this second check is
|
|
45
|
+
# a silent false negative rather than a crash, so it went unnoticed —
|
|
46
|
+
# `get_logger().info("auth", token=token)`, structlog's own documented
|
|
47
|
+
# module-level idiom, was unlinted by both SARJ012 and SARJ017.
|
|
48
|
+
callee = expr.func
|
|
49
|
+
if isinstance(callee, ast.Attribute) and callee.attr.lower() in _LOGGER_FACTORIES:
|
|
50
|
+
return True
|
|
51
|
+
if isinstance(callee, ast.Name) and callee.id.lower() in _LOGGER_FACTORIES:
|
|
52
|
+
return True
|
|
53
|
+
return is_logger_expr(callee)
|
|
54
|
+
return False
|
|
@@ -13,6 +13,8 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
import ast
|
|
15
15
|
|
|
16
|
+
from sarj_python_lint.rules._ast_index import walk
|
|
17
|
+
|
|
16
18
|
|
|
17
19
|
# The pytest-benchmark fixture: the test measures time, it does not verify.
|
|
18
20
|
_BENCHMARK = "benchmark"
|
|
@@ -33,7 +35,7 @@ def uses_benchmark_fixture(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool
|
|
|
33
35
|
"""
|
|
34
36
|
args = node.args
|
|
35
37
|
declared = any(arg.arg == _BENCHMARK for arg in (*args.posonlyargs, *args.args, *args.kwonlyargs))
|
|
36
|
-
return declared and any(isinstance(child, ast.Name) and child.id == _BENCHMARK for child in
|
|
38
|
+
return declared and any(isinstance(child, ast.Name) and child.id == _BENCHMARK for child in walk(node))
|
|
37
39
|
|
|
38
40
|
|
|
39
41
|
def has_benchmark_marker(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
@@ -69,7 +69,6 @@ from sarj_python_lint.rules.prefer_non_nullable_collection import (
|
|
|
69
69
|
PreferNonNullableCollection,
|
|
70
70
|
)
|
|
71
71
|
from sarj_python_lint.rules.prefer_or_pattern import PreferOrPattern
|
|
72
|
-
from sarj_python_lint.rules.prefer_pattern_matching import PreferPatternMatching
|
|
73
72
|
from sarj_python_lint.rules.prefer_real_store_in_tests import PreferRealStoreInTests
|
|
74
73
|
from sarj_python_lint.rules.prefer_self_type_annotation import PreferSelfTypeAnnotation
|
|
75
74
|
from sarj_python_lint.rules.prefer_str_enum import PreferStrEnum
|
|
@@ -84,9 +83,6 @@ from sarj_python_lint.rules.prefer_walrus_comprehension_filter import (
|
|
|
84
83
|
)
|
|
85
84
|
from sarj_python_lint.rules.prefer_walrus_regex_match import PreferWalrusRegexMatch
|
|
86
85
|
from sarj_python_lint.rules.prefer_walrus_stream_loop import PreferWalrusStreamLoop
|
|
87
|
-
from sarj_python_lint.rules.primary_export_file_name import (
|
|
88
|
-
PrimaryExportFileName,
|
|
89
|
-
)
|
|
90
86
|
from sarj_python_lint.rules.pydantic_at_boundaries import PydanticAtBoundaries
|
|
91
87
|
from sarj_python_lint.rules.redundant_docstring import RedundantDocstring
|
|
92
88
|
from sarj_python_lint.rules.require_port_for_service import RequirePortForService
|
|
@@ -173,11 +169,9 @@ REGISTRY: dict[str, type[Rule]] = {
|
|
|
173
169
|
PreferFstringOverConcat.id: PreferFstringOverConcat,
|
|
174
170
|
PreferMatchPatternDestructuring.id: PreferMatchPatternDestructuring,
|
|
175
171
|
PreferOrPattern.id: PreferOrPattern,
|
|
176
|
-
PreferPatternMatching.id: PreferPatternMatching,
|
|
177
172
|
RequirePortForService.id: RequirePortForService,
|
|
178
173
|
PreferNonNullableCollection.id: PreferNonNullableCollection,
|
|
179
174
|
PreferMatchTypeDispatch.id: PreferMatchTypeDispatch,
|
|
180
|
-
PrimaryExportFileName.id: PrimaryExportFileName,
|
|
181
175
|
PreferWalrusRegexMatch.id: PreferWalrusRegexMatch,
|
|
182
176
|
PreferWalrusComprehensionFilter.id: PreferWalrusComprehensionFilter,
|
|
183
177
|
PreferWalrusStreamLoop.id: PreferWalrusStreamLoop,
|
|
@@ -9,17 +9,9 @@ drifting apart on what "file-level" means.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
-
from dataclasses import dataclass
|
|
13
|
-
import io
|
|
14
|
-
import tokenize
|
|
12
|
+
from dataclasses import dataclass
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
# Sentinel row for "this file has no statement at all" (empty / comments-only):
|
|
18
|
-
# every comment then counts as preceding the first statement.
|
|
19
|
-
_NO_STATEMENT_LINE = 1 << 30
|
|
20
|
-
|
|
21
|
-
_LAYOUT_TOKENS = frozenset({tokenize.NL, tokenize.NEWLINE, tokenize.INDENT, tokenize.DEDENT})
|
|
22
|
-
_NON_STATEMENT_TOKENS = _LAYOUT_TOKENS | frozenset({tokenize.COMMENT, tokenize.ENCODING, tokenize.ENDMARKER})
|
|
14
|
+
from sarj_python_lint.rules._comments import all_comments
|
|
23
15
|
|
|
24
16
|
|
|
25
17
|
@dataclass(frozen=True, slots=True)
|
|
@@ -34,7 +26,7 @@ class Comment:
|
|
|
34
26
|
|
|
35
27
|
|
|
36
28
|
def scan_comments(source: str) -> list[Comment]:
|
|
37
|
-
"""
|
|
29
|
+
"""Describe every comment in `source`, in source order.
|
|
38
30
|
|
|
39
31
|
A comment is standalone when no token ended on its line before it, and
|
|
40
32
|
precedes the first statement when it sits above the first non-comment,
|
|
@@ -43,29 +35,28 @@ def scan_comments(source: str) -> list[Comment]:
|
|
|
43
35
|
Anything `tokenize` cannot lex propagates as `tokenize.TokenError`,
|
|
44
36
|
`IndentationError` or `SyntaxError`; callers treat that as "no diagnostics".
|
|
45
37
|
|
|
38
|
+
Built from `_comments.all_comments`, which is the same tokenize pass the
|
|
39
|
+
comment-hygiene rules already run. This module used to lex the file a second
|
|
40
|
+
time to compute exactly the same three facts from identical token-class
|
|
41
|
+
sets, which cost SARJ038 ~4% of total rule time for no additional
|
|
42
|
+
information. The memo now lives in one place rather than two.
|
|
43
|
+
|
|
46
44
|
Returns:
|
|
47
|
-
Every comment, in source order.
|
|
45
|
+
Every comment, in source order. `Comment` is frozen and the list is
|
|
46
|
+
read-only to callers.
|
|
48
47
|
|
|
49
48
|
"""
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
standalone=tok.start[0] != prev_end_row,
|
|
62
|
-
)
|
|
63
|
-
)
|
|
64
|
-
if tok.type not in _LAYOUT_TOKENS:
|
|
65
|
-
prev_end_row = tok.end[0]
|
|
66
|
-
if tok.type not in _NON_STATEMENT_TOKENS:
|
|
67
|
-
first_statement_line = min(first_statement_line, tok.start[0])
|
|
68
|
-
return [replace(c, before_first_statement=c.line < first_statement_line) for c in comments]
|
|
49
|
+
ordered, first_statement_line = all_comments(source)
|
|
50
|
+
return [
|
|
51
|
+
Comment(
|
|
52
|
+
line=line,
|
|
53
|
+
col=col + 1,
|
|
54
|
+
body=body,
|
|
55
|
+
standalone=standalone,
|
|
56
|
+
before_first_statement=line < first_statement_line,
|
|
57
|
+
)
|
|
58
|
+
for line, col, body, standalone in ordered
|
|
59
|
+
]
|
|
69
60
|
|
|
70
61
|
|
|
71
62
|
def comment_body(raw: str) -> str:
|
|
@@ -189,6 +189,7 @@ import re
|
|
|
189
189
|
from typing import TYPE_CHECKING, override
|
|
190
190
|
|
|
191
191
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
192
|
+
from sarj_python_lint.rules._ast_index import nodes, walk
|
|
192
193
|
from sarj_python_lint.rules._paths import is_test_path
|
|
193
194
|
|
|
194
195
|
|
|
@@ -407,7 +408,7 @@ def _is_exempt(node: ast.FunctionDef | ast.AsyncFunctionDef, helpers: frozenset[
|
|
|
407
408
|
if any(_decorator_name(dec) in _PROPERTY_DECORATORS for dec in node.decorator_list):
|
|
408
409
|
return True
|
|
409
410
|
for stmt in node.body:
|
|
410
|
-
for child in
|
|
411
|
+
for child in walk(stmt):
|
|
411
412
|
# `with self.subTest(...)` drives a table whose emptiness this rule
|
|
412
413
|
# cannot see; a nested `def` holding the assertions may be invoked
|
|
413
414
|
# by a runner (`asyncio.run`, a callback registry) the rule cannot
|
|
@@ -592,7 +593,7 @@ def _loop_guarantees(stmt: ast.For | ast.AsyncFor, facts: _Facts) -> bool:
|
|
|
592
593
|
|
|
593
594
|
|
|
594
595
|
def _bound_names(target: ast.expr) -> frozenset[str]:
|
|
595
|
-
return frozenset(node.id for node in
|
|
596
|
+
return frozenset(node.id for node in walk(target) if isinstance(node, ast.Name))
|
|
596
597
|
|
|
597
598
|
|
|
598
599
|
def _try_guarantees(stmt: ast.Try, facts: _Facts) -> bool:
|
|
@@ -622,7 +623,7 @@ def _is_capability_probe(test: ast.expr) -> bool:
|
|
|
622
623
|
True when the condition reads a capability flag or calls `hasattr`.
|
|
623
624
|
|
|
624
625
|
"""
|
|
625
|
-
for node in
|
|
626
|
+
for node in walk(test):
|
|
626
627
|
if isinstance(node, ast.Name) and _CAPABILITY_RE.search(node.id):
|
|
627
628
|
return True
|
|
628
629
|
if isinstance(node, ast.Attribute) and _CAPABILITY_RE.search(node.attr):
|
|
@@ -655,7 +656,7 @@ def _expr_names_assertion(expr: ast.expr, helpers: frozenset[str]) -> bool:
|
|
|
655
656
|
|
|
656
657
|
|
|
657
658
|
def _contains_assertion(node: ast.AST, helpers: frozenset[str]) -> bool:
|
|
658
|
-
return any(_is_assertion(child, helpers) for child in
|
|
659
|
+
return any(_is_assertion(child, helpers) for child in walk(node))
|
|
659
660
|
|
|
660
661
|
|
|
661
662
|
def _body_contains_assertion(node: ast.FunctionDef | ast.AsyncFunctionDef, helpers: frozenset[str]) -> bool:
|
|
@@ -666,7 +667,7 @@ def _body_contains_assertion(node: ast.FunctionDef | ast.AsyncFunctionDef, helpe
|
|
|
666
667
|
|
|
667
668
|
|
|
668
669
|
def _holds_assertion_call(stmt: ast.stmt, helpers: frozenset[str]) -> bool:
|
|
669
|
-
return any(_is_assertion(child, helpers) for child in
|
|
670
|
+
return any(_is_assertion(child, helpers) for child in walk(stmt))
|
|
670
671
|
|
|
671
672
|
|
|
672
673
|
def _is_assertion(child: ast.AST, helpers: frozenset[str]) -> bool:
|
|
@@ -711,9 +712,8 @@ def _asserting_helper_names(tree: ast.Module) -> frozenset[str]:
|
|
|
711
712
|
|
|
712
713
|
"""
|
|
713
714
|
defs: dict[str, ast.FunctionDef | ast.AsyncFunctionDef] = {}
|
|
714
|
-
for node in
|
|
715
|
-
|
|
716
|
-
defs.setdefault(node.name, node)
|
|
715
|
+
for node in nodes(tree, *_FUNC_NODES):
|
|
716
|
+
defs.setdefault(node.name, node)
|
|
717
717
|
verifying = {name for name, node in defs.items() if _contains_assertion(node, frozenset())}
|
|
718
718
|
pending = {name: _called_names(node) for name, node in defs.items() if name not in verifying}
|
|
719
719
|
while True:
|
|
@@ -726,7 +726,7 @@ def _asserting_helper_names(tree: ast.Module) -> frozenset[str]:
|
|
|
726
726
|
|
|
727
727
|
def _called_names(node: ast.AST) -> set[str]:
|
|
728
728
|
names: set[str] = set()
|
|
729
|
-
for child in
|
|
729
|
+
for child in walk(node):
|
|
730
730
|
if isinstance(child, ast.Call):
|
|
731
731
|
func = child.func
|
|
732
732
|
if isinstance(func, ast.Attribute):
|
|
@@ -779,7 +779,7 @@ def _facts_for(
|
|
|
779
779
|
helpers: frozenset[str],
|
|
780
780
|
) -> _Facts:
|
|
781
781
|
bindings = dict(module_bindings)
|
|
782
|
-
bindings.update(_bindings_in(list(
|
|
782
|
+
bindings.update(_bindings_in(list(walk(node))))
|
|
783
783
|
bindings.update(_default_bindings(node))
|
|
784
784
|
nonempty = _nonempty_claims(node, helpers) | set(module_nonempty) | _parametrized_nonempty(node)
|
|
785
785
|
base = _Facts(nonempty=frozenset(nonempty), bindings=bindings, imported=imported, helpers=helpers)
|
|
@@ -827,10 +827,10 @@ def _accumulators_filled(node: ast.FunctionDef | ast.AsyncFunctionDef, facts: _F
|
|
|
827
827
|
|
|
828
828
|
"""
|
|
829
829
|
filled: set[str] = set()
|
|
830
|
-
for child in
|
|
830
|
+
for child in walk(node):
|
|
831
831
|
if not isinstance(child, _LOOP_NODES) or not _iterable_is_nonempty(child.iter, facts, frozenset()):
|
|
832
832
|
continue
|
|
833
|
-
for inner in
|
|
833
|
+
for inner in walk(child):
|
|
834
834
|
if not (isinstance(inner, ast.Call) and isinstance(inner.func, ast.Attribute)):
|
|
835
835
|
continue
|
|
836
836
|
receiver = inner.func.value
|
|
@@ -847,9 +847,8 @@ def _imported_names(tree: ast.Module) -> frozenset[str]:
|
|
|
847
847
|
|
|
848
848
|
"""
|
|
849
849
|
names: set[str] = set()
|
|
850
|
-
for node in ast.
|
|
851
|
-
|
|
852
|
-
names.update(alias.asname or alias.name.split(".")[0] for alias in node.names)
|
|
850
|
+
for node in nodes(tree, ast.Import, ast.ImportFrom):
|
|
851
|
+
names.update(alias.asname or alias.name.split(".")[0] for alias in node.names)
|
|
853
852
|
return frozenset(names)
|
|
854
853
|
|
|
855
854
|
|
|
@@ -861,7 +860,7 @@ def _nonempty_claims(node: ast.AST, helpers: frozenset[str]) -> set[str]:
|
|
|
861
860
|
|
|
862
861
|
"""
|
|
863
862
|
nonempty: set[str] = set()
|
|
864
|
-
for child in
|
|
863
|
+
for child in walk(node):
|
|
865
864
|
if isinstance(child, ast.Assert):
|
|
866
865
|
nonempty |= _proves_nonempty(child.test)
|
|
867
866
|
elif isinstance(child, ast.Call) and _names_assertion(child.func, helpers):
|
|
@@ -198,6 +198,7 @@ import tokenize
|
|
|
198
198
|
from typing import TYPE_CHECKING, override
|
|
199
199
|
|
|
200
200
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
201
|
+
from sarj_python_lint.rules._ast_index import children
|
|
201
202
|
from sarj_python_lint.rules._comments import standalone_comments, trailing_comments
|
|
202
203
|
from sarj_python_lint.rules._paths import is_generated_source, is_test_path
|
|
203
204
|
|
|
@@ -656,7 +657,7 @@ def _decorator_shape(node: ast.FunctionDef | ast.AsyncFunctionDef) -> str:
|
|
|
656
657
|
|
|
657
658
|
def _walk(node: ast.AST) -> Iterator[ast.AST]:
|
|
658
659
|
yield node
|
|
659
|
-
for child in
|
|
660
|
+
for child in children(node):
|
|
660
661
|
yield from _walk(child)
|
|
661
662
|
|
|
662
663
|
|
|
@@ -51,6 +51,7 @@ import ast
|
|
|
51
51
|
from typing import TYPE_CHECKING, override
|
|
52
52
|
|
|
53
53
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
54
|
+
from sarj_python_lint.rules._ast_index import children, nodes
|
|
54
55
|
from sarj_python_lint.rules._paths import is_test_path
|
|
55
56
|
|
|
56
57
|
|
|
@@ -108,8 +109,8 @@ class FixtureReturnsBareTuple(Rule):
|
|
|
108
109
|
|
|
109
110
|
def _bare_tuple_results(tree: ast.Module) -> list[tuple[ast.expr, int]]:
|
|
110
111
|
hits: list[tuple[ast.expr, int]] = []
|
|
111
|
-
for node in
|
|
112
|
-
if not
|
|
112
|
+
for node in nodes(tree, *_FUNC_NODES):
|
|
113
|
+
if not _is_fixture(node):
|
|
113
114
|
continue
|
|
114
115
|
if _returns_distinctly_typed_tuple(node):
|
|
115
116
|
continue
|
|
@@ -175,7 +176,7 @@ def _scan_for_results(node: ast.AST) -> list[tuple[ast.expr, int]]:
|
|
|
175
176
|
count = _bare_tuple_arity(value)
|
|
176
177
|
if count >= _MIN_FIELDS:
|
|
177
178
|
found.append((value, count))
|
|
178
|
-
for child in
|
|
179
|
+
for child in children(node):
|
|
179
180
|
found.extend(_scan_for_results(child))
|
|
180
181
|
return found
|
|
181
182
|
|
|
@@ -48,6 +48,7 @@ import ast
|
|
|
48
48
|
from typing import TYPE_CHECKING, TypeGuard, override
|
|
49
49
|
|
|
50
50
|
from sarj_python_lint.rule_base import Diagnostic, Rule, parse_or_none
|
|
51
|
+
from sarj_python_lint.rules._ast_index import children, walk
|
|
51
52
|
from sarj_python_lint.rules._paths import is_generated_source
|
|
52
53
|
|
|
53
54
|
|
|
@@ -219,7 +220,7 @@ def _test_names(test: ast.expr) -> set[str]:
|
|
|
219
220
|
The unparsed reads appearing in the test expression.
|
|
220
221
|
|
|
221
222
|
"""
|
|
222
|
-
return {ast.unparse(n) for n in
|
|
223
|
+
return {ast.unparse(n) for n in walk(test) if isinstance(n, (ast.Name, ast.Attribute))}
|
|
223
224
|
|
|
224
225
|
|
|
225
226
|
def _loop_read_names(loop: ast.For | ast.AsyncFor | ast.While) -> frozenset[str]:
|
|
@@ -248,7 +249,7 @@ def _loop_read_names(loop: ast.For | ast.AsyncFor | ast.While) -> frozenset[str]
|
|
|
248
249
|
if other is not None:
|
|
249
250
|
stack.append(other)
|
|
250
251
|
continue
|
|
251
|
-
stack.extend(
|
|
252
|
+
stack.extend(children(node))
|
|
252
253
|
if isinstance(node, (ast.Name, ast.Attribute)) and isinstance(node.ctx, ast.Load):
|
|
253
254
|
reads.add(ast.unparse(node))
|
|
254
255
|
return frozenset(reads)
|
|
@@ -288,7 +289,7 @@ def _collect_reassignments(node: ast.AST, reassigns: dict[str, list[int]]) -> No
|
|
|
288
289
|
and not _is_accumulation_assign(node.target, node.value)
|
|
289
290
|
):
|
|
290
291
|
reassigns.setdefault(ast.unparse(node.target), []).append(node.target.lineno)
|
|
291
|
-
for child in
|
|
292
|
+
for child in children(node):
|
|
292
293
|
_collect_reassignments(child, reassigns)
|
|
293
294
|
|
|
294
295
|
|
|
@@ -364,7 +365,7 @@ def _collect_string_targets(node: ast.AST, names: set[str]) -> None:
|
|
|
364
365
|
and _looks_like_string(node.value)
|
|
365
366
|
):
|
|
366
367
|
names.add(node.target.id)
|
|
367
|
-
for child in
|
|
368
|
+
for child in children(node):
|
|
368
369
|
_collect_string_targets(child, names)
|
|
369
370
|
|
|
370
371
|
|