-
-
Notifications
You must be signed in to change notification settings - Fork 324
/
Copy pathast_utils.py
185 lines (153 loc) · 6.13 KB
/
ast_utils.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
from __future__ import annotations
import ast
from collections.abc import Sequence
from dataclasses import dataclass
from pathlib import Path
from textwrap import indent
from tokenize import COMMENT as COMMENT_TOKEN
from tokenize import generate_tokens
from typing import Any, Iterator
import click
from idom import html
def rewrite_changed_nodes(
file: Path,
source: str,
tree: ast.AST,
changed: list[ChangedNode],
) -> str:
ast.fix_missing_locations(tree)
lines = source.split("\n")
# find closest parent nodes that should be re-written
nodes_to_unparse: list[ast.AST] = []
for change in changed:
node_lineage = [change.node, *change.parents]
for i in range(len(node_lineage) - 1):
current_node, next_node = node_lineage[i : i + 2]
if (
not hasattr(next_node, "lineno")
or next_node.lineno < change.node.lineno
or isinstance(next_node, (ast.ClassDef, ast.FunctionDef))
):
nodes_to_unparse.append(current_node)
break
else: # pragma: no cover
raise RuntimeError("Failed to change code")
# check if an nodes to rewrite contain eachother, pick outermost nodes
current_outermost_node, *sorted_nodes_to_unparse = list(
sorted(nodes_to_unparse, key=lambda n: n.lineno)
)
outermost_nodes_to_unparse = [current_outermost_node]
for node in sorted_nodes_to_unparse:
if (
not current_outermost_node.end_lineno
or node.lineno > current_outermost_node.end_lineno
):
current_outermost_node = node
outermost_nodes_to_unparse.append(node)
moved_comment_lines_from_end: list[int] = []
# now actually rewrite these nodes (in reverse to avoid changes earlier in file)
for node in reversed(outermost_nodes_to_unparse):
# make a best effort to preserve any comments that we're going to overwrite
comments = _find_comments(lines[node.lineno - 1 : node.end_lineno])
# there may be some content just before and after the content we're re-writing
before_replacement = lines[node.lineno - 1][: node.col_offset].lstrip()
after_replacement = (
lines[node.end_lineno - 1][node.end_col_offset :].strip()
if node.end_lineno is not None and node.end_col_offset is not None
else ""
)
replacement = indent(
before_replacement
+ "\n".join([*comments, ast.unparse(node)])
+ after_replacement,
" " * (node.col_offset - len(before_replacement)),
)
lines[node.lineno - 1 : node.end_lineno or node.lineno] = [replacement]
if comments:
moved_comment_lines_from_end.append(len(lines) - node.lineno)
for lineno_from_end in sorted(list(set(moved_comment_lines_from_end))):
click.echo(f"Moved comments to {file}:{len(lines) - lineno_from_end}")
return "\n".join(lines)
@dataclass
class ChangedNode:
node: ast.AST
parents: Sequence[ast.AST]
def find_element_constructor_usages(
tree: ast.AST, add_props: bool = False
) -> Iterator[ElementConstructorInfo]:
changed: list[Sequence[ast.AST]] = []
for parents, node in _walk_with_parent(tree):
if not (isinstance(node, ast.Call)):
continue
func = node.func
if isinstance(func, ast.Attribute) and (
(isinstance(func.value, ast.Name) and func.value.id == "html")
or (isinstance(func.value, ast.Attribute) and func.value.attr == "html")
):
name = func.attr
elif isinstance(func, ast.Name):
name = func.id
else:
continue
maybe_attr_dict_node: Any | None = None
if name == "vdom":
if len(node.args) == 0:
continue
elif len(node.args) == 1:
maybe_attr_dict_node = ast.Dict(keys=[], values=[])
if add_props:
node.args.append(maybe_attr_dict_node)
else:
continue
elif isinstance(node.args[1], (ast.Constant, ast.JoinedStr)):
maybe_attr_dict_node = ast.Dict(keys=[], values=[])
if add_props:
node.args.insert(1, maybe_attr_dict_node)
else:
continue
elif len(node.args) >= 2:
maybe_attr_dict_node = node.args[1]
elif hasattr(html, name):
if len(node.args) == 0:
maybe_attr_dict_node = ast.Dict(keys=[], values=[])
if add_props:
node.args.append(maybe_attr_dict_node)
else:
continue
elif isinstance(node.args[0], (ast.Constant, ast.JoinedStr)):
maybe_attr_dict_node = ast.Dict(keys=[], values=[])
if add_props:
node.args.insert(0, maybe_attr_dict_node)
else:
continue
else:
maybe_attr_dict_node = node.args[0]
if not maybe_attr_dict_node:
continue
if isinstance(maybe_attr_dict_node, ast.Dict) or (
isinstance(maybe_attr_dict_node, ast.Call)
and isinstance(maybe_attr_dict_node.func, ast.Name)
and maybe_attr_dict_node.func.id == "dict"
and isinstance(maybe_attr_dict_node.func.ctx, ast.Load)
):
yield ElementConstructorInfo(node, maybe_attr_dict_node, parents)
return changed
@dataclass
class ElementConstructorInfo:
call: ast.Call
props: ast.Dict | ast.Call
parents: Sequence[ast.AST]
def _find_comments(lines: list[str]) -> list[str]:
iter_lines = iter(lines)
return [
token
for token_type, token, _, _, _ in generate_tokens(lambda: next(iter_lines))
if token_type == COMMENT_TOKEN
]
def _walk_with_parent(
node: ast.AST, parents: tuple[ast.AST, ...] = ()
) -> Iterator[tuple[tuple[ast.AST, ...], ast.AST]]:
parents = (node,) + parents
for child in ast.iter_child_nodes(node):
yield parents, child
yield from _walk_with_parent(child, parents)