code-towel 1.0.0__py3-none-any.whl
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.
- code_towel-1.0.0.dist-info/METADATA +722 -0
- code_towel-1.0.0.dist-info/RECORD +27 -0
- code_towel-1.0.0.dist-info/WHEEL +5 -0
- code_towel-1.0.0.dist-info/entry_points.txt +4 -0
- code_towel-1.0.0.dist-info/licenses/LICENSE +201 -0
- code_towel-1.0.0.dist-info/top_level.txt +1 -0
- towel/__init__.py +20 -0
- towel/cli.py +813 -0
- towel/unification/__init__.py +24 -0
- towel/unification/assignment_analyzer.py +403 -0
- towel/unification/ast_normalizer.py +223 -0
- towel/unification/ast_pretty_printer.py +133 -0
- towel/unification/binding_detector.py +381 -0
- towel/unification/block_signature.py +123 -0
- towel/unification/builtins.py +209 -0
- towel/unification/exceptions.py +113 -0
- towel/unification/extractor.py +884 -0
- towel/unification/models.py +190 -0
- towel/unification/nominal_unifier.py +375 -0
- towel/unification/orphan_detector.py +187 -0
- towel/unification/pipeline.py +524 -0
- towel/unification/project_layout.py +201 -0
- towel/unification/refactor_engine.py +3381 -0
- towel/unification/scope_analyzer.py +597 -0
- towel/unification/unifier.py +2128 -0
- towel/unification/visitor_utils.py +83 -0
- towel/unification/visitors.py +377 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Copyright 2025 Eric Allen
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Utilities for AST visitor classes to ensure complete coverage.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import ast
|
|
20
|
+
import os
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
from typing import Any, Callable
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def make_defensive_generic_visit(visitor_class_name: str) -> Callable[[Any, ast.AST], Any]:
|
|
27
|
+
"""
|
|
28
|
+
Create a defensive generic_visit method that catches unhandled AST nodes.
|
|
29
|
+
|
|
30
|
+
In development mode (when DEBUG_AST_COVERAGE=1), this raises an error
|
|
31
|
+
when an AST node type is encountered that doesn't have a specific visitor method.
|
|
32
|
+
This helps catch missing visitor methods during testing.
|
|
33
|
+
|
|
34
|
+
In production mode, it delegates to the parent's generic_visit.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
visitor_class_name: Name of the visitor class (for error messages)
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
A generic_visit method suitable for use in an AST visitor class
|
|
41
|
+
|
|
42
|
+
Example:
|
|
43
|
+
class MyVisitor(ast.NodeVisitor):
|
|
44
|
+
generic_visit = make_defensive_generic_visit('MyVisitor')
|
|
45
|
+
|
|
46
|
+
def visit_Name(self, node):
|
|
47
|
+
# ... handle Name nodes
|
|
48
|
+
return node
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def generic_visit(self: Any, node: ast.AST) -> Any:
|
|
52
|
+
"""
|
|
53
|
+
Fallback for unhandled nodes. In DEBUG mode, raises an error.
|
|
54
|
+
In production, delegates to parent.
|
|
55
|
+
"""
|
|
56
|
+
# Check if we're in debug mode
|
|
57
|
+
if os.getenv("DEBUG_AST_COVERAGE"):
|
|
58
|
+
# Get the list of visitor methods this class has
|
|
59
|
+
visitor_methods = {
|
|
60
|
+
name[6:]
|
|
61
|
+
for name in dir(self)
|
|
62
|
+
if name.startswith("visit_") and callable(getattr(self, name))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
node_type = node.__class__.__name__
|
|
66
|
+
|
|
67
|
+
# Only raise if this is a "real" AST node (not auxiliary types)
|
|
68
|
+
# and we don't have a visitor for it
|
|
69
|
+
if node_type not in visitor_methods and hasattr(ast, node_type):
|
|
70
|
+
raise NotImplementedError(
|
|
71
|
+
f"Missing visitor method for {node_type} in {visitor_class_name}. "
|
|
72
|
+
f"Please implement visit_{node_type}() or verify that "
|
|
73
|
+
f"generic traversal is appropriate for this node type."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# In production or for nodes we intentionally don't handle,
|
|
77
|
+
# delegate to the parent's generic_visit
|
|
78
|
+
if isinstance(self, ast.NodeTransformer):
|
|
79
|
+
return ast.NodeTransformer.generic_visit(self, node)
|
|
80
|
+
else:
|
|
81
|
+
return ast.NodeVisitor.generic_visit(self, node)
|
|
82
|
+
|
|
83
|
+
return generic_visit
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# Copyright 2025 Eric Allen
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Top-level AST visitors used by the refactoring engine.
|
|
17
|
+
|
|
18
|
+
These classes were extracted from nested definitions in refactor_engine.py to improve
|
|
19
|
+
readability and enable a clearer compiler-like pipeline structure.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import ast
|
|
25
|
+
from typing import Callable, List, Optional, Set, Tuple, Union, Literal, Sequence, TypeVar
|
|
26
|
+
|
|
27
|
+
MethodKind = Literal["instance", "classmethod", "staticmethod"]
|
|
28
|
+
T = TypeVar("T")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ClassCollector(ast.NodeVisitor):
|
|
32
|
+
"""Collect class metadata (qualname and base names) for a module tree.
|
|
33
|
+
|
|
34
|
+
Produces tuples of (qualname, bases) via the provided sink callback.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
file_path: str,
|
|
40
|
+
base_resolver: Callable[[ast.expr], Optional[str]],
|
|
41
|
+
sink: Callable[[str, List[str]], None],
|
|
42
|
+
) -> None:
|
|
43
|
+
self.file_path = file_path
|
|
44
|
+
self.base_resolver = base_resolver
|
|
45
|
+
self.sink = sink
|
|
46
|
+
self.class_stack: List[str] = []
|
|
47
|
+
|
|
48
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> None: # noqa: N802 (ast API)
|
|
49
|
+
qualname = ".".join(self.class_stack + [node.name]) if self.class_stack else node.name
|
|
50
|
+
bases: List[str] = []
|
|
51
|
+
for base in node.bases:
|
|
52
|
+
resolved = self.base_resolver(base)
|
|
53
|
+
if resolved:
|
|
54
|
+
bases.append(resolved)
|
|
55
|
+
self.sink(qualname, bases)
|
|
56
|
+
_push_value_and_visit(self.class_stack, node.name, self, node)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class FunctionCollector(ast.NodeVisitor):
|
|
60
|
+
"""Collect functions with enclosing class/function context for a module tree.
|
|
61
|
+
|
|
62
|
+
Calls sink with (node, class_name, enclosing_function, ancestry).
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
sink: Callable[
|
|
68
|
+
[Union[ast.FunctionDef, ast.AsyncFunctionDef], Optional[str], Optional[str], List[str]],
|
|
69
|
+
None,
|
|
70
|
+
],
|
|
71
|
+
) -> None:
|
|
72
|
+
self.sink = sink
|
|
73
|
+
self.class_stack: List[Optional[str]] = [None]
|
|
74
|
+
self.func_stack: List[Optional[str]] = [None]
|
|
75
|
+
|
|
76
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> None: # noqa: N802
|
|
77
|
+
_push_value_and_visit(self.class_stack, node.name, self, node)
|
|
78
|
+
|
|
79
|
+
def _record(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]) -> None:
|
|
80
|
+
ancestry = [n for n in self.func_stack if n is not None]
|
|
81
|
+
self.sink(node, self.class_stack[-1], self.func_stack[-1], ancestry)
|
|
82
|
+
|
|
83
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None: # noqa: N802
|
|
84
|
+
self._record(node)
|
|
85
|
+
_push_value_and_visit(self.func_stack, node.name, self, node)
|
|
86
|
+
|
|
87
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: # noqa: N802
|
|
88
|
+
self._record(node)
|
|
89
|
+
_push_value_and_visit(self.func_stack, node.name, self, node)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class MethodCallRewriter(ast.NodeTransformer):
|
|
93
|
+
"""Rewrite calls to a helper function into proper method dispatch syntax.
|
|
94
|
+
|
|
95
|
+
This mirrors the engine's previous inner-class behavior.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
drop_positional: Callable[[List[ast.expr], str], List[ast.expr]],
|
|
101
|
+
drop_keyword: Callable[[List[ast.keyword], str], List[ast.keyword]],
|
|
102
|
+
*,
|
|
103
|
+
original_name: str,
|
|
104
|
+
new_name: str,
|
|
105
|
+
method_kind: Optional[MethodKind],
|
|
106
|
+
implicit_name: Optional[str],
|
|
107
|
+
class_name: Optional[str],
|
|
108
|
+
) -> None:
|
|
109
|
+
self.drop_positional = drop_positional
|
|
110
|
+
self.drop_keyword = drop_keyword
|
|
111
|
+
self.original_name = original_name
|
|
112
|
+
self.new_name = new_name
|
|
113
|
+
self.method_kind = method_kind
|
|
114
|
+
self.implicit_name = implicit_name
|
|
115
|
+
self.class_name = class_name
|
|
116
|
+
|
|
117
|
+
def visit_Call(self, n: ast.Call) -> ast.AST:
|
|
118
|
+
self.generic_visit(n)
|
|
119
|
+
if isinstance(n.func, ast.Name) and n.func.id == self.original_name and self.method_kind:
|
|
120
|
+
implicit_name = self.implicit_name or (
|
|
121
|
+
"self" if self.method_kind == "instance" else "cls"
|
|
122
|
+
)
|
|
123
|
+
if self.method_kind in {"instance", "classmethod"}:
|
|
124
|
+
n.args = self.drop_positional(n.args, implicit_name)
|
|
125
|
+
n.keywords = self.drop_keyword(n.keywords, implicit_name)
|
|
126
|
+
attr = ast.Attribute(
|
|
127
|
+
value=ast.Name(id=implicit_name, ctx=ast.Load()),
|
|
128
|
+
attr=self.new_name,
|
|
129
|
+
ctx=ast.Load(),
|
|
130
|
+
)
|
|
131
|
+
ast.copy_location(attr, n.func)
|
|
132
|
+
n.func = attr
|
|
133
|
+
elif self.method_kind == "staticmethod" and self.class_name:
|
|
134
|
+
attr = ast.Attribute(
|
|
135
|
+
value=ast.Name(id=self.class_name, ctx=ast.Load()),
|
|
136
|
+
attr=self.new_name,
|
|
137
|
+
ctx=ast.Load(),
|
|
138
|
+
)
|
|
139
|
+
ast.copy_location(attr, n.func)
|
|
140
|
+
n.func = attr
|
|
141
|
+
return n
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class LoopReturnFinder(ast.NodeVisitor):
|
|
145
|
+
"""Detect whether a code block contains return statements inside loops.
|
|
146
|
+
|
|
147
|
+
Used to identify control flow patterns that may prevent safe refactoring.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def __init__(self) -> None:
|
|
151
|
+
self.has_loop_return = False
|
|
152
|
+
self.in_loop = False
|
|
153
|
+
|
|
154
|
+
def visit_For(self, node: ast.For) -> None: # noqa: N802
|
|
155
|
+
_visit_loop_and_restore_flag(self, node)
|
|
156
|
+
|
|
157
|
+
def visit_While(self, node: ast.While) -> None: # noqa: N802
|
|
158
|
+
_visit_loop_and_restore_flag(self, node)
|
|
159
|
+
|
|
160
|
+
def visit_Return(self, node: ast.Return) -> None: # noqa: N802
|
|
161
|
+
if self.in_loop:
|
|
162
|
+
self.has_loop_return = True
|
|
163
|
+
|
|
164
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None: # noqa: N802
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: # noqa: N802
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class NameCollector(ast.NodeVisitor):
|
|
172
|
+
"""Collect all names referenced in Load context within a code block.
|
|
173
|
+
|
|
174
|
+
Stops at nested function boundaries to avoid capturing scopes outside the block.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
def __init__(self) -> None:
|
|
178
|
+
self.used: Set[str] = set()
|
|
179
|
+
|
|
180
|
+
def visit_Name(self, n: ast.Name) -> None: # noqa: N802
|
|
181
|
+
_record_load_name_and_visit(n, self.used, self)
|
|
182
|
+
|
|
183
|
+
def visit_FunctionDef(self, n: ast.FunctionDef) -> None: # noqa: N802
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
def visit_AsyncFunctionDef(self, n: ast.AsyncFunctionDef) -> None: # noqa: N802
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class AugAssignFinder(ast.NodeVisitor):
|
|
191
|
+
"""Find all variables modified by augmented assignment operators (+=, -=, etc.).
|
|
192
|
+
|
|
193
|
+
Stops at nested function boundaries to avoid capturing scopes outside the block.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
def __init__(self) -> None:
|
|
197
|
+
self.aug_assign_targets: Set[str] = set()
|
|
198
|
+
|
|
199
|
+
def visit_AugAssign(self, node: ast.AugAssign) -> None: # noqa: N802
|
|
200
|
+
_record_simple_assignment(node, self.aug_assign_targets, self)
|
|
201
|
+
|
|
202
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None: # noqa: N802
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: # noqa: N802
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class AssignTargetVisitor(ast.NodeVisitor):
|
|
210
|
+
"""Collect all assignment targets and global/nonlocal declarations in a block.
|
|
211
|
+
|
|
212
|
+
Tracks simple name assignments from Assign, AugAssign, and AnnAssign nodes,
|
|
213
|
+
plus global and nonlocal declarations. Stops at nested function boundaries.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
def __init__(self) -> None:
|
|
217
|
+
self.assigned_names: Set[str] = set()
|
|
218
|
+
self.declared_global_in_block: Set[str] = set()
|
|
219
|
+
self.declared_nonlocal_in_block: Set[str] = set()
|
|
220
|
+
|
|
221
|
+
def visit_Assign(self, node: ast.Assign) -> None: # noqa: N802
|
|
222
|
+
for t in node.targets:
|
|
223
|
+
_record_name_target(t, self.assigned_names)
|
|
224
|
+
self.generic_visit(node)
|
|
225
|
+
|
|
226
|
+
def visit_AugAssign(self, node: ast.AugAssign) -> None: # noqa: N802
|
|
227
|
+
_record_simple_assignment(node, self.assigned_names, self)
|
|
228
|
+
|
|
229
|
+
def visit_AnnAssign(self, node: ast.AnnAssign) -> None: # noqa: N802
|
|
230
|
+
_record_simple_assignment(node, self.assigned_names, self)
|
|
231
|
+
|
|
232
|
+
def visit_Global(self, node: ast.Global) -> None: # noqa: N802
|
|
233
|
+
for n in node.names:
|
|
234
|
+
self.declared_global_in_block.add(n)
|
|
235
|
+
|
|
236
|
+
def visit_Nonlocal(self, node: ast.Nonlocal) -> None: # noqa: N802
|
|
237
|
+
for n in node.names:
|
|
238
|
+
self.declared_nonlocal_in_block.add(n)
|
|
239
|
+
|
|
240
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None: # noqa: N802
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: # noqa: N802
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class ClassLocator(ast.NodeVisitor):
|
|
248
|
+
"""Locate a class definition by name and determine its insertion point.
|
|
249
|
+
|
|
250
|
+
Returns the line number and indentation suitable for inserting a new method
|
|
251
|
+
at the end of the target class body.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
def __init__(self, source: str, target_name: str) -> None:
|
|
255
|
+
self.source = source
|
|
256
|
+
self.target_name = target_name
|
|
257
|
+
self.result: Optional[Tuple[int, str]] = None
|
|
258
|
+
|
|
259
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> None: # noqa: N802
|
|
260
|
+
if node.name == self.target_name and hasattr(node, "end_lineno"):
|
|
261
|
+
indent = _compute_indent(self.source, node.lineno)
|
|
262
|
+
end_lineno = getattr(node, "end_lineno", None)
|
|
263
|
+
if isinstance(end_lineno, int):
|
|
264
|
+
insert_line = end_lineno - 1
|
|
265
|
+
else:
|
|
266
|
+
insert_line = node.lineno
|
|
267
|
+
self.result = (insert_line, indent)
|
|
268
|
+
self.generic_visit(node)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class FuncLocator(ast.NodeVisitor):
|
|
272
|
+
"""Locate a function definition by name and determine its helper insertion point.
|
|
273
|
+
|
|
274
|
+
Returns the line number and indentation suitable for inserting a new helper
|
|
275
|
+
function inside the target function, after any existing nested definitions
|
|
276
|
+
but before the first executable statement.
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
def __init__(self, source: str, function_name: str) -> None:
|
|
280
|
+
self.source = source
|
|
281
|
+
self.function_name = function_name
|
|
282
|
+
self.result: Optional[Tuple[int, str]] = None
|
|
283
|
+
|
|
284
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None: # noqa: N802
|
|
285
|
+
self._maybe_capture_insertion_point(node)
|
|
286
|
+
|
|
287
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: # noqa: N802
|
|
288
|
+
self._maybe_capture_insertion_point(node)
|
|
289
|
+
|
|
290
|
+
def _maybe_capture_insertion_point(
|
|
291
|
+
self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]
|
|
292
|
+
) -> None:
|
|
293
|
+
if node.name != self.function_name:
|
|
294
|
+
self.generic_visit(node)
|
|
295
|
+
return
|
|
296
|
+
indent = _compute_indent(self.source, node.lineno)
|
|
297
|
+
body = _body_without_docstring(node.body)
|
|
298
|
+
insert_line: Optional[int] = None
|
|
299
|
+
last_def_end: Optional[int] = None
|
|
300
|
+
for stmt in body:
|
|
301
|
+
if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
302
|
+
stmt_end = getattr(stmt, "end_lineno", stmt.lineno)
|
|
303
|
+
if isinstance(stmt_end, int):
|
|
304
|
+
last_def_end = stmt_end
|
|
305
|
+
continue
|
|
306
|
+
insert_line = stmt.lineno - 1
|
|
307
|
+
break
|
|
308
|
+
if insert_line is None:
|
|
309
|
+
if last_def_end is not None:
|
|
310
|
+
insert_line = last_def_end
|
|
311
|
+
else:
|
|
312
|
+
insert_line = node.lineno
|
|
313
|
+
self.result = (insert_line, indent)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _push_value_and_visit(
|
|
317
|
+
stack: List[T], value: T, visitor: ast.NodeVisitor, node: ast.AST
|
|
318
|
+
) -> None:
|
|
319
|
+
stack.append(value)
|
|
320
|
+
try:
|
|
321
|
+
visitor.generic_visit(node)
|
|
322
|
+
finally:
|
|
323
|
+
stack.pop()
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _visit_loop_and_restore_flag(
|
|
327
|
+
visitor: "LoopReturnFinder", node: Union[ast.For, ast.While]
|
|
328
|
+
) -> None:
|
|
329
|
+
previous_flag = visitor.in_loop
|
|
330
|
+
visitor.in_loop = True
|
|
331
|
+
visitor.generic_visit(node)
|
|
332
|
+
visitor.in_loop = previous_flag
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _record_load_name_and_visit(
|
|
336
|
+
node: ast.Name, destination: Set[str], visitor: ast.NodeVisitor
|
|
337
|
+
) -> None:
|
|
338
|
+
if isinstance(node.ctx, ast.Load):
|
|
339
|
+
destination.add(node.id)
|
|
340
|
+
visitor.generic_visit(node)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _record_name_target(target: ast.AST, destination: Set[str]) -> None:
|
|
344
|
+
if isinstance(target, ast.Name):
|
|
345
|
+
destination.add(target.id)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _record_simple_assignment(
|
|
349
|
+
node: Union[ast.AnnAssign, ast.AugAssign], destination: Set[str], visitor: ast.AST
|
|
350
|
+
) -> None:
|
|
351
|
+
target = getattr(node, "target", None)
|
|
352
|
+
if isinstance(target, ast.Name):
|
|
353
|
+
destination.add(target.id)
|
|
354
|
+
visitor.generic_visit(node)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _body_without_docstring(body: Sequence[ast.stmt]) -> List[ast.stmt]:
|
|
358
|
+
body_list = list(body)
|
|
359
|
+
if not body_list:
|
|
360
|
+
return []
|
|
361
|
+
|
|
362
|
+
first_stmt = body_list[0]
|
|
363
|
+
if (
|
|
364
|
+
isinstance(first_stmt, ast.Expr)
|
|
365
|
+
and isinstance(first_stmt.value, ast.Constant)
|
|
366
|
+
and isinstance(first_stmt.value.value, str)
|
|
367
|
+
):
|
|
368
|
+
return body_list[1:]
|
|
369
|
+
|
|
370
|
+
return body_list
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _compute_indent(source: str, lineno: int) -> str:
|
|
374
|
+
lines = source.splitlines()
|
|
375
|
+
index = max(0, min(len(lines) - 1, lineno - 1))
|
|
376
|
+
line = lines[index] if lines else ""
|
|
377
|
+
return line[: len(line) - len(line.lstrip())]
|