leyline-code 0.8.1__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.
- leyline/__init__.py +3 -0
- leyline/adapters/__init__.py +19 -0
- leyline/adapters/csharp.py +1026 -0
- leyline/adapters/generic.py +844 -0
- leyline/adapters/python.py +929 -0
- leyline/adapters/typescript.py +1512 -0
- leyline/affected.py +509 -0
- leyline/agent_skills.py +311 -0
- leyline/change.py +500 -0
- leyline/channels.py +1782 -0
- leyline/cli.py +1029 -0
- leyline/cluster.py +149 -0
- leyline/context.py +531 -0
- leyline/coupling.py +485 -0
- leyline/coverage.py +376 -0
- leyline/diagrams.py +680 -0
- leyline/diff.py +1078 -0
- leyline/drift.py +747 -0
- leyline/exact.py +410 -0
- leyline/explain.py +973 -0
- leyline/export.py +673 -0
- leyline/fncoupling.py +328 -0
- leyline/formats.py +923 -0
- leyline/grade.py +207 -0
- leyline/incremental.py +931 -0
- leyline/indexer.py +3733 -0
- leyline/learnings.py +743 -0
- leyline/loop.py +584 -0
- leyline/model.py +142 -0
- leyline/outline.py +1066 -0
- leyline/packing.py +86 -0
- leyline/patterns.py +478 -0
- leyline/pr.py +1249 -0
- leyline/props.py +255 -0
- leyline/query.py +683 -0
- leyline/quick.py +853 -0
- leyline/related.py +266 -0
- leyline/removal.py +231 -0
- leyline/renames.py +231 -0
- leyline/rereview.py +375 -0
- leyline/roslyn_refs/Program.cs +179 -0
- leyline/roslyn_refs/RoslynRefs.csproj +16 -0
- leyline/rules.py +171 -0
- leyline/schema.sql +178 -0
- leyline/scip_pb2.py +97 -0
- leyline/server.py +1230 -0
- leyline/skills/leyline-adversarial-review/SKILL.md +121 -0
- leyline/skills/leyline-ask/SKILL.md +57 -0
- leyline/skills/leyline-change-impact/SKILL.md +79 -0
- leyline/skills/leyline-explain-flow/SKILL.md +70 -0
- leyline/skills/leyline-explore-module/SKILL.md +55 -0
- leyline/skills/leyline-pr-review/SKILL.md +77 -0
- leyline/skills/leyline-quick-change/SKILL.md +63 -0
- leyline/skills/leyline-spec/SKILL.md +179 -0
- leyline/skills/leyline-tour/SKILL.md +46 -0
- leyline/spec.py +1819 -0
- leyline/store.py +560 -0
- leyline/tours.py +352 -0
- leyline/verdicts.py +390 -0
- leyline/viewer/viewer.html +1622 -0
- leyline_code-0.8.1.dist-info/METADATA +1325 -0
- leyline_code-0.8.1.dist-info/RECORD +65 -0
- leyline_code-0.8.1.dist-info/WHEEL +4 -0
- leyline_code-0.8.1.dist-info/entry_points.txt +2 -0
- leyline_code-0.8.1.dist-info/licenses/LICENSE +674 -0
leyline/formats.py
ADDED
|
@@ -0,0 +1,923 @@
|
|
|
1
|
+
"""Code joined by a string's shape, not by a call: one function builds a key such as `dialogue/<d>/nodes/<n>/text`,
|
|
2
|
+
and another takes keys of that shape apart. Nothing calls across, so a change to the shape on one side breaks
|
|
3
|
+
the other side silently.
|
|
4
|
+
|
|
5
|
+
`extract` finds both sides in a parsed file and records them as `format` endpoints:
|
|
6
|
+
|
|
7
|
+
- a writer is a template with holes and at least two fixed parts between separators (`/` or `:`): a template
|
|
8
|
+
literal, an f-string, an interpolated string, `"%s/nodes/%s" % ...`, `"{}/nodes/{}".format(...)`,
|
|
9
|
+
`String.Format("{0}/nodes/{1}", ...)` or a chain of `+`. A hole that is a call to another key builder
|
|
10
|
+
(`${dialogueNodeKey(d, n)}/text`) is filled in with that builder's shape when the two are linked.
|
|
11
|
+
- a reader is a regular expression over such a shape (`/^dialogue\\/([^/]+)\\/nodes\\//`), a startsWith, endsWith
|
|
12
|
+
or includes test of a literal with two or more fixed parts, or a function that compares the pieces of a split
|
|
13
|
+
key by position (`parts[2] === "nodes"`, `rest[0] !== "text"`).
|
|
14
|
+
|
|
15
|
+
`resolve` links a writer to a reader when two or more fixed parts of the reader sit at the same places in the
|
|
16
|
+
writer's shape and no fixed part disagrees. Routes (`/api/...`) and file paths (a part with a dot, a leading `/`)
|
|
17
|
+
are left to the http and file channels. The link is data-like: the writer's end is the hub, and a reader must
|
|
18
|
+
read what the writer now writes."""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from collections import Counter, defaultdict
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
from .model import Edge
|
|
27
|
+
|
|
28
|
+
WORD = re.compile(r"[A-Za-z_][A-Za-z0-9_\-]*\Z")
|
|
29
|
+
DIGITS = re.compile(r"\d+\Z")
|
|
30
|
+
SEPS = ("/", ":")
|
|
31
|
+
HOLE = "*"
|
|
32
|
+
# First parts that make a shape a URL or a path, which other channels own.
|
|
33
|
+
NOT_KEYS = frozenset("api http https www v1 v2 v3 static assets public src dist build node_modules tmp var usr etc home".split()
|
|
34
|
+
# git's own names (refs/heads/x): both ends talk to git, not to each other
|
|
35
|
+
+ ["refs"])
|
|
36
|
+
PREFIX_TESTS = {"startsWith": "start", "startswith": "start", "StartsWith": "start", "endsWith": "end", "endswith": "end",
|
|
37
|
+
"EndsWith": "end", "includes": "any", "Contains": "any", "indexOf": "any", "IndexOf": "any"}
|
|
38
|
+
REGEX_FNS = {"compile", "match", "fullmatch", "search", "findall", "finditer", "sub", "subn", "split"}
|
|
39
|
+
CS_REGEX_FNS = {"Match", "IsMatch", "Matches", "Replace", "Split"}
|
|
40
|
+
TOO_MANY_WRITERS = 12 # one shape built in this many places is a convention, not one format's two ends
|
|
41
|
+
TOO_MANY_READERS = 25
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _t(node) -> str:
|
|
45
|
+
return node.text.decode("utf8", "replace") if node is not None else ""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# -- shapes ------------------------------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
def shape_of(pieces: list) -> Optional[tuple[str, list[str]]]:
|
|
51
|
+
"""(separator, parts) for a template given as pieces: ("c", text) for fixed text, ("h", None) for a hole,
|
|
52
|
+
("call", name) for a hole that is a call, ("v", value) for a hole filled by a known constant. A part is fixed
|
|
53
|
+
text, `*` for a hole, or `@name` for a call to fill in later. None when the template is not a key."""
|
|
54
|
+
text = "".join(p[1] if p[0] in ("c", "v") else "\x00" for p in pieces)
|
|
55
|
+
if not text or len(text) > 200 or "://" in text or text.startswith(("/", "http", "\\")) or "\n" in text:
|
|
56
|
+
return None
|
|
57
|
+
lead_call = bool(pieces) and pieces[0][0] == "call"
|
|
58
|
+
sep = next((s for s in SEPS if text.count(s) >= 2 or (lead_call and text.count(s) >= 1)), None)
|
|
59
|
+
if sep is None:
|
|
60
|
+
return None
|
|
61
|
+
segs: list[list] = [[]]
|
|
62
|
+
for kind, val in pieces:
|
|
63
|
+
if kind in ("c", "v"):
|
|
64
|
+
bits = val.split(sep)
|
|
65
|
+
for i, b in enumerate(bits):
|
|
66
|
+
if i:
|
|
67
|
+
segs.append([])
|
|
68
|
+
if b:
|
|
69
|
+
segs[-1].append(("c", b))
|
|
70
|
+
else:
|
|
71
|
+
segs[-1].append((kind, val))
|
|
72
|
+
if segs and not segs[-1]:
|
|
73
|
+
segs.pop() # a trailing separator
|
|
74
|
+
out = []
|
|
75
|
+
for seg in segs:
|
|
76
|
+
if not seg:
|
|
77
|
+
return None # a leading or doubled separator: a path
|
|
78
|
+
if all(k == "c" for k, _ in seg):
|
|
79
|
+
word = "".join(v for _, v in seg)
|
|
80
|
+
if not (WORD.match(word) or DIGITS.match(word)):
|
|
81
|
+
return None # spaces, dots, quotes: prose, a file name, markup
|
|
82
|
+
out.append(word)
|
|
83
|
+
elif len(seg) == 1 and seg[0][0] == "call":
|
|
84
|
+
out.append("@" + seg[0][1])
|
|
85
|
+
else:
|
|
86
|
+
fixed = "".join(v for k, v in seg if k == "c")
|
|
87
|
+
if fixed and not re.fullmatch(r"[A-Za-z0-9_\-]+", fixed):
|
|
88
|
+
return None
|
|
89
|
+
out.append(HOLE)
|
|
90
|
+
return sep, out
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def is_key(parts: list[str]) -> bool:
|
|
94
|
+
"""A finished writer shape: fixed first part, two or more fixed words, at least one hole."""
|
|
95
|
+
words = [p for p in parts if WORD.match(p)]
|
|
96
|
+
return (len(parts) >= 3 and bool(WORD.match(parts[0])) and parts[0].lower() not in NOT_KEYS
|
|
97
|
+
and len(words) >= 2 and HOLE in parts and not any(p.startswith("@") for p in parts))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def address(sep: str, parts: list[str]) -> str:
|
|
101
|
+
return sep.join(parts)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def regex_shape(pattern: str) -> Optional[tuple[str, list[str], bool]]:
|
|
105
|
+
"""(separator, parts, anchored) for a regular expression over a key: ^dialogue\\/([^/]+)\\/nodes -> dialogue/*/nodes.
|
|
106
|
+
Only a plain sequence of words and groups is read; alternation at the top, or a part with a dot, is not."""
|
|
107
|
+
if len(pattern) > 300:
|
|
108
|
+
return None
|
|
109
|
+
anchored = pattern.startswith("^")
|
|
110
|
+
body = pattern[1:] if anchored else pattern
|
|
111
|
+
body = re.sub(r"\$$|\\z$|\\Z$", "", body)
|
|
112
|
+
sep = "/" if body.count("/") >= 2 else ":" if body.count(":") >= 2 else None
|
|
113
|
+
if sep is None:
|
|
114
|
+
return None
|
|
115
|
+
segs, cur, depth, cls, i = [], "", 0, False, 0
|
|
116
|
+
while i < len(body):
|
|
117
|
+
ch = body[i]
|
|
118
|
+
if ch == "\\" and i + 1 < len(body):
|
|
119
|
+
nxt = body[i + 1]
|
|
120
|
+
if nxt == sep and depth == 0 and not cls:
|
|
121
|
+
segs.append(cur)
|
|
122
|
+
cur, i = "", i + 2
|
|
123
|
+
continue
|
|
124
|
+
cur += ch + nxt
|
|
125
|
+
i += 2
|
|
126
|
+
continue
|
|
127
|
+
if cls:
|
|
128
|
+
cls = ch != "]"
|
|
129
|
+
elif ch == "[":
|
|
130
|
+
cls = True
|
|
131
|
+
elif ch == "(":
|
|
132
|
+
depth += 1
|
|
133
|
+
elif ch == ")":
|
|
134
|
+
depth -= 1
|
|
135
|
+
elif ch == "|" and depth == 0:
|
|
136
|
+
return None
|
|
137
|
+
elif ch == sep and depth == 0:
|
|
138
|
+
segs.append(cur)
|
|
139
|
+
cur, i = "", i + 1
|
|
140
|
+
continue
|
|
141
|
+
cur += ch
|
|
142
|
+
i += 1
|
|
143
|
+
segs.append(cur)
|
|
144
|
+
if segs and segs[-1] == "":
|
|
145
|
+
segs.pop()
|
|
146
|
+
if not segs or segs[0] == "":
|
|
147
|
+
return None
|
|
148
|
+
out = []
|
|
149
|
+
for s in segs:
|
|
150
|
+
plain = s.replace("\\-", "-").replace("\\_", "_")
|
|
151
|
+
if WORD.match(plain):
|
|
152
|
+
out.append(plain)
|
|
153
|
+
elif "\\." in s or re.search(r"(?<!\\)\.\w", s) and not re.search(r"[(\[]", s):
|
|
154
|
+
return None # a file name
|
|
155
|
+
elif re.search(r"[(\[.\\+*?{]", s):
|
|
156
|
+
out.append(HOLE)
|
|
157
|
+
else:
|
|
158
|
+
return None
|
|
159
|
+
words = [p for p in out if WORD.match(p)]
|
|
160
|
+
if len(words) < 2 or HOLE not in out:
|
|
161
|
+
return None
|
|
162
|
+
return sep, out, anchored
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def literal_shape(text: str) -> Optional[tuple[str, list[str]]]:
|
|
166
|
+
"""(separator, fixed parts) of a literal a key is tested against: "dialogue/" -> no (one part); "nodes/x/" -> two."""
|
|
167
|
+
sep = next((s for s in SEPS if s in text), None)
|
|
168
|
+
if sep is None or len(text) > 120 or any(ch.isspace() for ch in text):
|
|
169
|
+
return None
|
|
170
|
+
parts = [p for p in text.split(sep)]
|
|
171
|
+
inner = [p for p in parts if p]
|
|
172
|
+
if len(inner) < 2 or not all(WORD.match(p) for p in inner) or text.startswith("/") and inner[0].lower() in NOT_KEYS:
|
|
173
|
+
return None
|
|
174
|
+
if parts[0] == "" and text.startswith("/") and len(parts) > 1 and parts[1].lower() in NOT_KEYS:
|
|
175
|
+
return None
|
|
176
|
+
return sep, inner
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# -- extraction --------------------------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
class _Base:
|
|
182
|
+
"""Walks one file and records format ends. Subclasses say how each language writes the shapes."""
|
|
183
|
+
|
|
184
|
+
def __init__(self, tree, where, out, consts: dict):
|
|
185
|
+
self.tree, self.where, self.out = tree, where, out
|
|
186
|
+
self.consts = {k: v[0] for k, v in consts.items() if len(v) == 1 and v[0] and WORD.match(v[0])}
|
|
187
|
+
self.index: dict[tuple, dict] = defaultdict(lambda: defaultdict(set)) # (fn, array) -> position -> words
|
|
188
|
+
self.index_line: dict[tuple, int] = {}
|
|
189
|
+
self.alias: dict[tuple, tuple] = {} # (fn, name) -> (array, position)
|
|
190
|
+
self.origin: dict[tuple, Optional[int]] = {} # (fn, array) -> offset into a split key, None when unknown
|
|
191
|
+
self.splits: dict[str, set] = defaultdict(set) # fn -> separators it splits keys on
|
|
192
|
+
self.done: set = set()
|
|
193
|
+
self.held: dict[str, tuple] = {} # a module constant or field holding a pattern -> the pattern's shape
|
|
194
|
+
|
|
195
|
+
def fn(self, node) -> Optional[str]:
|
|
196
|
+
return self.where.fn(node.start_point[0] + 1)
|
|
197
|
+
|
|
198
|
+
def writer(self, node, pieces) -> None:
|
|
199
|
+
if (node.start_byte, node.end_byte) in self.done:
|
|
200
|
+
return
|
|
201
|
+
self.done.add((node.start_byte, node.end_byte))
|
|
202
|
+
got = shape_of(pieces)
|
|
203
|
+
src = self.fn(node)
|
|
204
|
+
if got is None or src is None:
|
|
205
|
+
return
|
|
206
|
+
sep, parts = got
|
|
207
|
+
parts = parts[:1] + [HOLE if p.startswith("@") else p for p in parts[1:]] # only a leading builder is filled in
|
|
208
|
+
if not (parts[0].startswith("@") and len(parts) >= 2) and not is_key(parts):
|
|
209
|
+
return
|
|
210
|
+
self.out.add("format", "write", src, address(sep, parts), node.start_point[0] + 1, "template")
|
|
211
|
+
|
|
212
|
+
def reader(self, node, sep, parts, kind, anchored=False) -> None:
|
|
213
|
+
src = self.fn(node)
|
|
214
|
+
if src is None:
|
|
215
|
+
return
|
|
216
|
+
n = self.where.nodes.get(src)
|
|
217
|
+
if n is not None and (n.kind == "type" or n.name in ("<module>", "<top-level>")):
|
|
218
|
+
# A pattern kept in a module constant or a field is read where the code uses it.
|
|
219
|
+
holder, cur = None, node
|
|
220
|
+
for _ in range(4):
|
|
221
|
+
cur = cur.parent
|
|
222
|
+
if cur is None:
|
|
223
|
+
break
|
|
224
|
+
if cur.type in ("assignment", "variable_declarator"):
|
|
225
|
+
name = cur.child_by_field_name("left") or cur.child_by_field_name("name") \
|
|
226
|
+
or next((c for c in cur.named_children if c.type == "identifier"), None)
|
|
227
|
+
holder = _t(name).rsplit(".", 1)[-1] if name is not None else None
|
|
228
|
+
break
|
|
229
|
+
if holder and WORD.match(holder):
|
|
230
|
+
self.held[holder] = (sep, parts, kind, anchored, src, node.start_point[0] + 1)
|
|
231
|
+
return
|
|
232
|
+
self.out.add("format", "read", src, address(sep, parts), node.start_point[0] + 1, kind,
|
|
233
|
+
["anchored"] if anchored else [])
|
|
234
|
+
|
|
235
|
+
def uses_of_held(self) -> None:
|
|
236
|
+
if not self.held:
|
|
237
|
+
return
|
|
238
|
+
used = set()
|
|
239
|
+
stack = [self.tree.root_node]
|
|
240
|
+
while stack:
|
|
241
|
+
n = stack.pop()
|
|
242
|
+
if n.type in ("identifier", "property_identifier") and _t(n) in self.held:
|
|
243
|
+
src = self.fn(n)
|
|
244
|
+
node = self.where.nodes.get(src) if src else None
|
|
245
|
+
if node is not None and node.kind in ("callable", "test") and node.name not in ("<module>", "<top-level>"):
|
|
246
|
+
sep, parts, kind, anchored, _s, _l = self.held[_t(n)]
|
|
247
|
+
used.add(_t(n))
|
|
248
|
+
self.out.add("format", "read", src, address(sep, parts), n.start_point[0] + 1, kind,
|
|
249
|
+
["anchored"] if anchored else [])
|
|
250
|
+
stack.extend(n.children)
|
|
251
|
+
for name, (sep, parts, kind, anchored, src, line) in self.held.items():
|
|
252
|
+
if name not in used: # used nowhere in this file: the constant itself is the reader
|
|
253
|
+
self.out.add("format", "read", src, address(sep, parts), line, kind, ["anchored"] if anchored else [])
|
|
254
|
+
|
|
255
|
+
def compare(self, node, sub_array: str, pos: int, words: list[str]) -> None:
|
|
256
|
+
src = self.fn(node)
|
|
257
|
+
if src is None:
|
|
258
|
+
return
|
|
259
|
+
key = (src, sub_array)
|
|
260
|
+
if key in self.alias:
|
|
261
|
+
sub_array, base = self.alias[key]
|
|
262
|
+
pos += base
|
|
263
|
+
key = (src, sub_array)
|
|
264
|
+
for w in words:
|
|
265
|
+
if WORD.match(w):
|
|
266
|
+
self.index[key][pos].add(w)
|
|
267
|
+
self.index_line.setdefault(key, node.start_point[0] + 1)
|
|
268
|
+
|
|
269
|
+
def finish(self) -> None:
|
|
270
|
+
self.uses_of_held()
|
|
271
|
+
for (src, arr), at in self.index.items():
|
|
272
|
+
if len(at) < 2:
|
|
273
|
+
continue
|
|
274
|
+
off = self.origin.get((src, arr), "?")
|
|
275
|
+
seps = self.splits.get(src) or set(SEPS)
|
|
276
|
+
lits = [f"{p}={'|'.join(sorted(ws))}" for p, ws in sorted(at.items())]
|
|
277
|
+
if isinstance(off, int):
|
|
278
|
+
lits.append(f"offset={off}")
|
|
279
|
+
for sep in sorted(seps):
|
|
280
|
+
shown = " ".join(f"[{p}]={'|'.join(sorted(ws))}" for p, ws in sorted(at.items()))
|
|
281
|
+
self.out.add("format", "read", src, f"{arr}{sep} {shown}", self.index_line[(src, arr)], "index", lits)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class _TypeScript(_Base):
|
|
285
|
+
def pieces(self, node) -> Optional[list]:
|
|
286
|
+
t = node.type
|
|
287
|
+
if t == "template_string":
|
|
288
|
+
out = []
|
|
289
|
+
for c in node.children:
|
|
290
|
+
if c.type in ("string_fragment", "escape_sequence"):
|
|
291
|
+
out.append(("c", _t(c)))
|
|
292
|
+
elif c.type == "template_substitution":
|
|
293
|
+
out.append(self.hole(c.named_children[0] if c.named_children else None))
|
|
294
|
+
return out
|
|
295
|
+
if t == "string":
|
|
296
|
+
return [("c", "".join(_t(c) for c in node.children if c.type in ("string_fragment", "escape_sequence")))]
|
|
297
|
+
if t == "parenthesized_expression" and node.named_children:
|
|
298
|
+
return self.pieces(node.named_children[0])
|
|
299
|
+
if t == "binary_expression" and _t(node.child_by_field_name("operator")) == "+":
|
|
300
|
+
a, b = self.pieces(node.child_by_field_name("left")), self.pieces(node.child_by_field_name("right"))
|
|
301
|
+
return (a or [("h", None)]) + (b or [("h", None)]) if (a or b) else None
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
def hole(self, e):
|
|
305
|
+
if e is None:
|
|
306
|
+
return ("h", None)
|
|
307
|
+
if e.type == "identifier" and _t(e) in self.consts:
|
|
308
|
+
return ("v", self.consts[_t(e)])
|
|
309
|
+
if e.type == "call_expression":
|
|
310
|
+
f = e.child_by_field_name("function")
|
|
311
|
+
if f is not None and f.type == "identifier":
|
|
312
|
+
return ("call", _t(f))
|
|
313
|
+
return ("h", None)
|
|
314
|
+
|
|
315
|
+
def run(self) -> None:
|
|
316
|
+
stack = [self.tree.root_node]
|
|
317
|
+
while stack:
|
|
318
|
+
n = stack.pop()
|
|
319
|
+
t = n.type
|
|
320
|
+
if t == "template_string" and any(c.type == "template_substitution" for c in n.children):
|
|
321
|
+
self.writer(n, self.pieces(n) or [])
|
|
322
|
+
elif t == "binary_expression":
|
|
323
|
+
op = _t(n.child_by_field_name("operator"))
|
|
324
|
+
if op == "+" and not (n.parent is not None and n.parent.type == "binary_expression"
|
|
325
|
+
and _t(n.parent.child_by_field_name("operator")) == "+"):
|
|
326
|
+
p = self.pieces(n)
|
|
327
|
+
if p and any(k == "c" for k, _ in p) and any(k != "c" for k, _ in p):
|
|
328
|
+
self.writer(n, p)
|
|
329
|
+
elif op in ("===", "!==", "==", "!="):
|
|
330
|
+
self._compare(n, n.child_by_field_name("left"), n.child_by_field_name("right"))
|
|
331
|
+
self._compare(n, n.child_by_field_name("right"), n.child_by_field_name("left"))
|
|
332
|
+
elif t == "regex":
|
|
333
|
+
got = regex_shape(_t(n.child_by_field_name("pattern")))
|
|
334
|
+
if got:
|
|
335
|
+
self.reader(n, got[0], got[1], "regex", got[2])
|
|
336
|
+
elif t == "call_expression":
|
|
337
|
+
self._call(n)
|
|
338
|
+
elif t == "variable_declarator":
|
|
339
|
+
self._declare(n)
|
|
340
|
+
elif t == "switch_statement":
|
|
341
|
+
self._switch(n)
|
|
342
|
+
stack.extend(reversed(n.children))
|
|
343
|
+
self.finish()
|
|
344
|
+
|
|
345
|
+
def _sub(self, node):
|
|
346
|
+
"""(array, position) for a[2]."""
|
|
347
|
+
while node is not None and node.type in ("parenthesized_expression", "non_null_expression"):
|
|
348
|
+
node = node.named_children[0] if node.named_children else None
|
|
349
|
+
if node is not None and node.type == "subscript_expression":
|
|
350
|
+
obj, idx = node.child_by_field_name("object"), node.child_by_field_name("index")
|
|
351
|
+
if obj is not None and obj.type == "identifier" and idx is not None and idx.type == "number" and _t(idx).isdigit():
|
|
352
|
+
return _t(obj), int(_t(idx))
|
|
353
|
+
if node is not None and node.type == "identifier":
|
|
354
|
+
key = (self.fn(node), _t(node))
|
|
355
|
+
if key in self.alias:
|
|
356
|
+
return self.alias[key][0], self.alias[key][1]
|
|
357
|
+
return None
|
|
358
|
+
|
|
359
|
+
def _compare(self, n, side, other) -> None:
|
|
360
|
+
at = self._sub(side)
|
|
361
|
+
if at is None or other is None or other.type != "string":
|
|
362
|
+
return
|
|
363
|
+
self.compare(n, at[0], at[1], [(self.pieces(other) or [("c", "")])[0][1]])
|
|
364
|
+
|
|
365
|
+
def _switch(self, n) -> None:
|
|
366
|
+
at = self._sub(n.child_by_field_name("value"))
|
|
367
|
+
body = n.child_by_field_name("body")
|
|
368
|
+
if at is None or body is None:
|
|
369
|
+
return
|
|
370
|
+
words = [_t(v)[1:-1] for c in body.named_children if c.type == "switch_case"
|
|
371
|
+
for v in [c.child_by_field_name("value")] if v is not None and v.type == "string"]
|
|
372
|
+
self.compare(n, at[0], at[1], words)
|
|
373
|
+
|
|
374
|
+
def _declare(self, n) -> None:
|
|
375
|
+
name, value = n.child_by_field_name("name"), n.child_by_field_name("value")
|
|
376
|
+
if name is None or value is None:
|
|
377
|
+
return
|
|
378
|
+
src = self.fn(n)
|
|
379
|
+
while value.type in ("non_null_expression", "parenthesized_expression", "as_expression") and value.named_children:
|
|
380
|
+
value = value.named_children[0]
|
|
381
|
+
if name.type == "identifier":
|
|
382
|
+
at = self._sub(value)
|
|
383
|
+
if at is not None:
|
|
384
|
+
self.alias[(src, _t(name))] = at
|
|
385
|
+
return
|
|
386
|
+
if value.type == "call_expression":
|
|
387
|
+
f = value.child_by_field_name("function")
|
|
388
|
+
args = value.child_by_field_name("arguments")
|
|
389
|
+
if f is not None and f.type == "member_expression":
|
|
390
|
+
prop = _t(f.child_by_field_name("property"))
|
|
391
|
+
obj = f.child_by_field_name("object")
|
|
392
|
+
if prop == "split":
|
|
393
|
+
self.origin[(src, _t(name))] = 0
|
|
394
|
+
elif prop == "slice" and obj is not None and obj.type == "identifier" and args is not None \
|
|
395
|
+
and args.named_children and args.named_children[0].type == "number" and _t(args.named_children[0]).isdigit():
|
|
396
|
+
self.alias[(src, _t(name))] = (_t(obj), int(_t(args.named_children[0]))) # rest = parts.slice(2)
|
|
397
|
+
elif name.type == "array_pattern" and value.type == "call_expression":
|
|
398
|
+
f = value.child_by_field_name("function")
|
|
399
|
+
if f is not None and f.type == "member_expression" and _t(f.child_by_field_name("property")) == "split":
|
|
400
|
+
arr = f"[{name.start_point[0]}]"
|
|
401
|
+
self.origin[(src, arr)] = 0
|
|
402
|
+
for i, c in enumerate(name.named_children):
|
|
403
|
+
if c.type == "identifier":
|
|
404
|
+
self.alias[(src, _t(c))] = (arr, i)
|
|
405
|
+
elif c.type == "rest_pattern" and c.named_children and c.named_children[0].type == "identifier":
|
|
406
|
+
self.alias[(src, _t(c.named_children[0]))] = (arr, i) # [kind, id, ...rest]
|
|
407
|
+
|
|
408
|
+
def _call(self, n) -> None:
|
|
409
|
+
f = n.child_by_field_name("function")
|
|
410
|
+
args = n.child_by_field_name("arguments")
|
|
411
|
+
if f is None or f.type != "member_expression" or args is None or not args.named_children:
|
|
412
|
+
return
|
|
413
|
+
prop = _t(f.child_by_field_name("property"))
|
|
414
|
+
first = args.named_children[0]
|
|
415
|
+
if prop == "split" and first.type == "string":
|
|
416
|
+
s = _t(first)[1:-1]
|
|
417
|
+
if s in SEPS:
|
|
418
|
+
src = self.fn(n)
|
|
419
|
+
if src:
|
|
420
|
+
self.splits[src].add(s)
|
|
421
|
+
elif prop in PREFIX_TESTS and first.type == "string":
|
|
422
|
+
got = literal_shape(_t(first)[1:-1])
|
|
423
|
+
if got:
|
|
424
|
+
self.reader(n, got[0], got[1], PREFIX_TESTS[prop], PREFIX_TESTS[prop] == "start")
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class _Python(_Base):
|
|
428
|
+
def pieces(self, node) -> Optional[list]:
|
|
429
|
+
t = node.type
|
|
430
|
+
if t == "string":
|
|
431
|
+
out = []
|
|
432
|
+
for c in node.children:
|
|
433
|
+
if c.type in ("string_content", "escape_sequence"):
|
|
434
|
+
out.append(("c", _t(c)))
|
|
435
|
+
elif c.type == "interpolation":
|
|
436
|
+
e = c.named_children[0] if c.named_children else None
|
|
437
|
+
out.append(self.hole(e))
|
|
438
|
+
return out
|
|
439
|
+
if t == "concatenated_string":
|
|
440
|
+
return [p for c in node.named_children for p in (self.pieces(c) or [])]
|
|
441
|
+
if t == "parenthesized_expression" and node.named_children:
|
|
442
|
+
return self.pieces(node.named_children[0])
|
|
443
|
+
if t == "binary_operator" and _t(node.child_by_field_name("operator")) == "+":
|
|
444
|
+
a, b = self.pieces(node.child_by_field_name("left")), self.pieces(node.child_by_field_name("right"))
|
|
445
|
+
return (a or [("h", None)]) + (b or [("h", None)]) if (a or b) else None
|
|
446
|
+
return None
|
|
447
|
+
|
|
448
|
+
def hole(self, e):
|
|
449
|
+
if e is None:
|
|
450
|
+
return ("h", None)
|
|
451
|
+
if e.type == "identifier" and _t(e) in self.consts:
|
|
452
|
+
return ("v", self.consts[_t(e)])
|
|
453
|
+
if e.type == "call":
|
|
454
|
+
f = e.child_by_field_name("function")
|
|
455
|
+
if f is not None and f.type == "identifier":
|
|
456
|
+
return ("call", _t(f))
|
|
457
|
+
return ("h", None)
|
|
458
|
+
|
|
459
|
+
@staticmethod
|
|
460
|
+
def holes(text: str) -> list:
|
|
461
|
+
"""A %-format or str.format template as pieces: "%s/nodes/%s" -> hole, "/nodes/", hole."""
|
|
462
|
+
out, last = [], 0
|
|
463
|
+
for m in re.finditer(r"%[-#0 +]*\d*(?:\.\d+)?[sdirf]|\{[^{}]*\}", text):
|
|
464
|
+
out.append(("c", text[last:m.start()]))
|
|
465
|
+
out.append(("h", None))
|
|
466
|
+
last = m.end()
|
|
467
|
+
out.append(("c", text[last:]))
|
|
468
|
+
return [p for p in out if p != ("c", "")]
|
|
469
|
+
|
|
470
|
+
def run(self) -> None:
|
|
471
|
+
stack = [self.tree.root_node]
|
|
472
|
+
while stack:
|
|
473
|
+
n = stack.pop()
|
|
474
|
+
t = n.type
|
|
475
|
+
if t == "string" and any(c.type == "interpolation" for c in n.children):
|
|
476
|
+
self.writer(n, self.pieces(n) or [])
|
|
477
|
+
elif t == "binary_operator":
|
|
478
|
+
op = _t(n.child_by_field_name("operator"))
|
|
479
|
+
left = n.child_by_field_name("left")
|
|
480
|
+
if op == "%" and left is not None and left.type == "string":
|
|
481
|
+
self.writer(n, self.holes("".join(v for _, v in (self.pieces(left) or []) if v)))
|
|
482
|
+
elif op == "+" and not (n.parent is not None and n.parent.type == "binary_operator"
|
|
483
|
+
and _t(n.parent.child_by_field_name("operator")) == "+"):
|
|
484
|
+
p = self.pieces(n)
|
|
485
|
+
if p and any(k == "c" for k, _ in p) and any(k != "c" for k, _ in p):
|
|
486
|
+
self.writer(n, p)
|
|
487
|
+
elif t == "comparison_operator":
|
|
488
|
+
self._compare(n)
|
|
489
|
+
elif t == "call":
|
|
490
|
+
self._call(n)
|
|
491
|
+
elif t == "assignment":
|
|
492
|
+
self._assign(n)
|
|
493
|
+
stack.extend(reversed(n.children))
|
|
494
|
+
self.finish()
|
|
495
|
+
|
|
496
|
+
def _sub(self, node):
|
|
497
|
+
if node is not None and node.type == "subscript":
|
|
498
|
+
obj, idx = node.child_by_field_name("value"), node.child_by_field_name("subscript")
|
|
499
|
+
if obj is not None and obj.type == "identifier" and idx is not None and idx.type == "integer":
|
|
500
|
+
return _t(obj), int(_t(idx))
|
|
501
|
+
if node is not None and node.type == "identifier":
|
|
502
|
+
key = (self.fn(node), _t(node))
|
|
503
|
+
if key in self.alias:
|
|
504
|
+
return self.alias[key]
|
|
505
|
+
return None
|
|
506
|
+
|
|
507
|
+
def _compare(self, n) -> None:
|
|
508
|
+
kids = n.children
|
|
509
|
+
if len(kids) != 3:
|
|
510
|
+
return
|
|
511
|
+
a, op, b = kids
|
|
512
|
+
if _t(op) not in ("==", "!=", "in", "not in"):
|
|
513
|
+
return
|
|
514
|
+
for side, other in ((a, b), (b, a)):
|
|
515
|
+
at = self._sub(side)
|
|
516
|
+
if at is None:
|
|
517
|
+
continue
|
|
518
|
+
if other.type == "string":
|
|
519
|
+
words = ["".join(_t(c) for c in other.children if c.type == "string_content")]
|
|
520
|
+
elif other.type in ("tuple", "list", "set") and _t(op) in ("in", "not in"):
|
|
521
|
+
words = ["".join(_t(c) for c in s.children if c.type == "string_content") for s in other.named_children
|
|
522
|
+
if s.type == "string"]
|
|
523
|
+
else:
|
|
524
|
+
continue
|
|
525
|
+
self.compare(n, at[0], at[1], words)
|
|
526
|
+
return
|
|
527
|
+
|
|
528
|
+
def _assign(self, n) -> None:
|
|
529
|
+
left, right = n.child_by_field_name("left"), n.child_by_field_name("right")
|
|
530
|
+
if left is None or right is None:
|
|
531
|
+
return
|
|
532
|
+
src = self.fn(n)
|
|
533
|
+
if left.type == "identifier":
|
|
534
|
+
at = self._sub(right)
|
|
535
|
+
if at is not None:
|
|
536
|
+
self.alias[(src, _t(left))] = at
|
|
537
|
+
return
|
|
538
|
+
if right.type == "call":
|
|
539
|
+
f = right.child_by_field_name("function")
|
|
540
|
+
if f is not None and f.type == "attribute" and _t(f.child_by_field_name("attribute")) in ("split", "rsplit"):
|
|
541
|
+
self.origin[(src, _t(left))] = 0
|
|
542
|
+
elif right.type == "subscript":
|
|
543
|
+
obj, sl = right.child_by_field_name("value"), right.child_by_field_name("subscript")
|
|
544
|
+
if obj is not None and obj.type == "identifier" and sl is not None and sl.type == "slice":
|
|
545
|
+
lo = sl.named_children[0] if sl.named_children else None
|
|
546
|
+
if lo is not None and lo.type == "integer" and _t(sl).startswith(_t(lo)):
|
|
547
|
+
self.alias[(src, _t(left))] = (_t(obj), int(_t(lo)))
|
|
548
|
+
elif left.type in ("pattern_list", "tuple_pattern") and right.type == "call":
|
|
549
|
+
f = right.child_by_field_name("function")
|
|
550
|
+
if f is not None and f.type == "attribute" and _t(f.child_by_field_name("attribute")) == "split":
|
|
551
|
+
arr = f"[{left.start_point[0]}]"
|
|
552
|
+
self.origin[(src, arr)] = 0
|
|
553
|
+
for i, c in enumerate(left.named_children):
|
|
554
|
+
if c.type == "identifier":
|
|
555
|
+
self.alias[(src, _t(c))] = (arr, i)
|
|
556
|
+
|
|
557
|
+
def _call(self, n) -> None:
|
|
558
|
+
f = n.child_by_field_name("function")
|
|
559
|
+
args = n.child_by_field_name("arguments")
|
|
560
|
+
if f is None or args is None or not args.named_children:
|
|
561
|
+
return
|
|
562
|
+
first = args.named_children[0]
|
|
563
|
+
name = _t(f.child_by_field_name("attribute")) if f.type == "attribute" else _t(f)
|
|
564
|
+
if f.type == "attribute" and name == "format" and f.child_by_field_name("object") is not None \
|
|
565
|
+
and f.child_by_field_name("object").type == "string":
|
|
566
|
+
obj = f.child_by_field_name("object")
|
|
567
|
+
self.writer(n, self.holes("".join(v for _, v in (self.pieces(obj) or []) if v)))
|
|
568
|
+
return
|
|
569
|
+
if first.type != "string" or any(c.type == "interpolation" for c in first.children):
|
|
570
|
+
return
|
|
571
|
+
text = "".join(_t(c) for c in first.children if c.type == "string_content")
|
|
572
|
+
if f.type == "attribute" and name in ("split", "rsplit") and text in SEPS:
|
|
573
|
+
src = self.fn(n)
|
|
574
|
+
if src:
|
|
575
|
+
self.splits[src].add(text)
|
|
576
|
+
elif f.type == "attribute" and name in PREFIX_TESTS:
|
|
577
|
+
got = literal_shape(text)
|
|
578
|
+
if got:
|
|
579
|
+
self.reader(n, got[0], got[1], PREFIX_TESTS[name], PREFIX_TESTS[name] == "start")
|
|
580
|
+
elif name in REGEX_FNS and f.type == "attribute" and _t(f.child_by_field_name("object")) in ("re", "regex"):
|
|
581
|
+
got = regex_shape(text)
|
|
582
|
+
if got:
|
|
583
|
+
self.reader(n, got[0], got[1], "regex", got[2] or name in ("match", "fullmatch"))
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
class _CSharp(_Base):
|
|
587
|
+
def pieces(self, node) -> Optional[list]:
|
|
588
|
+
t = node.type
|
|
589
|
+
if t == "interpolated_string_expression":
|
|
590
|
+
out = []
|
|
591
|
+
for c in node.children:
|
|
592
|
+
if c.type == "interpolation":
|
|
593
|
+
e = next((x for x in c.named_children if not x.type.startswith("interpolation_")), None)
|
|
594
|
+
out.append(self.hole(e))
|
|
595
|
+
elif c.is_named and not c.type.startswith("interpolation_"):
|
|
596
|
+
out.append(("c", _t(c)))
|
|
597
|
+
return out
|
|
598
|
+
if t in ("string_literal", "verbatim_string_literal", "raw_string_literal"):
|
|
599
|
+
return [("c", self.text(node))]
|
|
600
|
+
if t == "parenthesized_expression" and node.named_children:
|
|
601
|
+
return self.pieces(node.named_children[0])
|
|
602
|
+
if t == "binary_expression" and _t(node.child_by_field_name("operator")) == "+":
|
|
603
|
+
a, b = self.pieces(node.child_by_field_name("left")), self.pieces(node.child_by_field_name("right"))
|
|
604
|
+
return (a or [("h", None)]) + (b or [("h", None)]) if (a or b) else None
|
|
605
|
+
return None
|
|
606
|
+
|
|
607
|
+
@staticmethod
|
|
608
|
+
def text(node) -> str:
|
|
609
|
+
body = "".join(_t(c) for c in node.children if c.type.endswith("content"))
|
|
610
|
+
return body or _t(node).lstrip("@").strip('"')
|
|
611
|
+
|
|
612
|
+
def hole(self, e):
|
|
613
|
+
if e is None:
|
|
614
|
+
return ("h", None)
|
|
615
|
+
if e.type == "identifier" and _t(e) in self.consts:
|
|
616
|
+
return ("v", self.consts[_t(e)])
|
|
617
|
+
if e.type == "invocation_expression":
|
|
618
|
+
f = e.child_by_field_name("function")
|
|
619
|
+
if f is not None and f.type == "identifier":
|
|
620
|
+
return ("call", _t(f))
|
|
621
|
+
return ("h", None)
|
|
622
|
+
|
|
623
|
+
def run(self) -> None:
|
|
624
|
+
stack = [self.tree.root_node]
|
|
625
|
+
while stack:
|
|
626
|
+
n = stack.pop()
|
|
627
|
+
t = n.type
|
|
628
|
+
if t == "interpolated_string_expression":
|
|
629
|
+
self.writer(n, self.pieces(n) or [])
|
|
630
|
+
elif t == "binary_expression":
|
|
631
|
+
op = _t(n.child_by_field_name("operator"))
|
|
632
|
+
if op == "+" and not (n.parent is not None and n.parent.type == "binary_expression"
|
|
633
|
+
and _t(n.parent.child_by_field_name("operator")) == "+"):
|
|
634
|
+
p = self.pieces(n)
|
|
635
|
+
if p and any(k == "c" for k, _ in p) and any(k != "c" for k, _ in p):
|
|
636
|
+
self.writer(n, p)
|
|
637
|
+
elif op in ("==", "!="):
|
|
638
|
+
for side, other in ((n.child_by_field_name("left"), n.child_by_field_name("right")),
|
|
639
|
+
(n.child_by_field_name("right"), n.child_by_field_name("left"))):
|
|
640
|
+
at = self._sub(side)
|
|
641
|
+
if at is not None and other is not None and other.type == "string_literal":
|
|
642
|
+
self.compare(n, at[0], at[1], [self.text(other)])
|
|
643
|
+
break
|
|
644
|
+
elif t == "invocation_expression":
|
|
645
|
+
self._call(n)
|
|
646
|
+
elif t == "object_creation_expression":
|
|
647
|
+
ty = _t(n.child_by_field_name("type")).rsplit(".", 1)[-1]
|
|
648
|
+
args = n.child_by_field_name("arguments")
|
|
649
|
+
first = self._args(args)[:1]
|
|
650
|
+
if ty == "Regex" and first and first[0].type in ("string_literal", "verbatim_string_literal", "raw_string_literal"):
|
|
651
|
+
got = regex_shape(self.text(first[0]))
|
|
652
|
+
if got:
|
|
653
|
+
self.reader(n, got[0], got[1], "regex", got[2])
|
|
654
|
+
elif t == "variable_declarator":
|
|
655
|
+
self._declare(n)
|
|
656
|
+
elif t == "attribute" and _t(n.child_by_field_name("name")) in ("GeneratedRegex", "GeneratedRegexAttribute"):
|
|
657
|
+
for a in n.named_children:
|
|
658
|
+
for s in a.named_children if a.type == "attribute_argument_list" else ():
|
|
659
|
+
lit = s.named_children[0] if s.named_children else None
|
|
660
|
+
if lit is not None and lit.type in ("string_literal", "verbatim_string_literal"):
|
|
661
|
+
got = regex_shape(self.text(lit))
|
|
662
|
+
if got:
|
|
663
|
+
self.reader(n, got[0], got[1], "regex", got[2])
|
|
664
|
+
break
|
|
665
|
+
stack.extend(reversed(n.children))
|
|
666
|
+
self.finish()
|
|
667
|
+
|
|
668
|
+
@staticmethod
|
|
669
|
+
def _args(args) -> list:
|
|
670
|
+
return [a.named_children[-1] for a in args.children if a.type == "argument" and a.named_children] if args is not None else []
|
|
671
|
+
|
|
672
|
+
def fn(self, node):
|
|
673
|
+
return self.where.fn(node.start_point[0] + 1) or self.where.type(node.start_point[0] + 1)
|
|
674
|
+
|
|
675
|
+
def _sub(self, node):
|
|
676
|
+
if node is not None and node.type == "element_access_expression":
|
|
677
|
+
obj = node.child_by_field_name("expression")
|
|
678
|
+
sub = node.child_by_field_name("subscript")
|
|
679
|
+
idx = _t(sub).strip("[] ") if sub is not None else ""
|
|
680
|
+
if obj is not None and obj.type == "identifier" and idx.isdigit():
|
|
681
|
+
return _t(obj), int(idx)
|
|
682
|
+
if node is not None and node.type == "identifier":
|
|
683
|
+
key = (self.fn(node), _t(node))
|
|
684
|
+
if key in self.alias:
|
|
685
|
+
return self.alias[key]
|
|
686
|
+
return None
|
|
687
|
+
|
|
688
|
+
def _declare(self, n) -> None:
|
|
689
|
+
name = n.child_by_field_name("name") or next((c for c in n.named_children if c.type == "identifier"), None)
|
|
690
|
+
value = n.named_children[-1] if n.named_child_count > 1 else None
|
|
691
|
+
if value is not None and value.type == "equals_value_clause":
|
|
692
|
+
value = value.named_children[-1] if value.named_children else None
|
|
693
|
+
if name is None or value is None:
|
|
694
|
+
return
|
|
695
|
+
src = self.fn(n)
|
|
696
|
+
at = self._sub(value)
|
|
697
|
+
if at is not None:
|
|
698
|
+
self.alias[(src, _t(name))] = at
|
|
699
|
+
elif value.type == "invocation_expression":
|
|
700
|
+
f = value.child_by_field_name("function")
|
|
701
|
+
if f is not None and f.type == "member_access_expression" and _t(f.child_by_field_name("name")) == "Split":
|
|
702
|
+
self.origin[(src, _t(name))] = 0
|
|
703
|
+
|
|
704
|
+
def _call(self, n) -> None:
|
|
705
|
+
f = n.child_by_field_name("function")
|
|
706
|
+
args = self._args(n.child_by_field_name("arguments"))
|
|
707
|
+
if f is None or not args:
|
|
708
|
+
return
|
|
709
|
+
name = _t(f.child_by_field_name("name")) if f.type == "member_access_expression" else _t(f)
|
|
710
|
+
owner = _t(f.child_by_field_name("expression")) if f.type == "member_access_expression" else ""
|
|
711
|
+
first = args[0]
|
|
712
|
+
if name == "Format" and owner in ("string", "String") and first.type in ("string_literal", "verbatim_string_literal"):
|
|
713
|
+
text = self.text(first)
|
|
714
|
+
out, last = [], 0
|
|
715
|
+
for m in re.finditer(r"\{\d+(?:[,:][^}]*)?\}", text):
|
|
716
|
+
out += [("c", text[last:m.start()]), ("h", None)]
|
|
717
|
+
last = m.end()
|
|
718
|
+
out.append(("c", text[last:]))
|
|
719
|
+
self.writer(n, [p for p in out if p != ("c", "")])
|
|
720
|
+
return
|
|
721
|
+
if name == "Split" and first.type in ("character_literal", "string_literal"):
|
|
722
|
+
sep = _t(first)[1:-1]
|
|
723
|
+
src = self.fn(n)
|
|
724
|
+
if sep in SEPS and src:
|
|
725
|
+
self.splits[src].add(sep)
|
|
726
|
+
elif name in PREFIX_TESTS and first.type in ("string_literal", "verbatim_string_literal"):
|
|
727
|
+
got = literal_shape(self.text(first))
|
|
728
|
+
if got:
|
|
729
|
+
self.reader(n, got[0], got[1], PREFIX_TESTS[name], PREFIX_TESTS[name] == "start")
|
|
730
|
+
elif name in CS_REGEX_FNS and owner == "Regex" and len(args) > 1 \
|
|
731
|
+
and args[1].type in ("string_literal", "verbatim_string_literal", "raw_string_literal"):
|
|
732
|
+
got = regex_shape(self.text(args[1]))
|
|
733
|
+
if got:
|
|
734
|
+
self.reader(n, got[0], got[1], "regex", got[2])
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def extract(lang: str, tree, where, out, consts: Optional[dict] = None) -> None:
|
|
738
|
+
"""Record the format ends in one parsed file (see the module docstring). A failure loses this file's formats."""
|
|
739
|
+
cls = {"typescript": _TypeScript, "python": _Python, "csharp": _CSharp}.get(lang)
|
|
740
|
+
if cls is None:
|
|
741
|
+
return
|
|
742
|
+
try:
|
|
743
|
+
cls(tree, where, out, consts or {}).run()
|
|
744
|
+
except RecursionError:
|
|
745
|
+
pass
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
# -- linking -----------------------------------------------------------------------------------------------
|
|
749
|
+
|
|
750
|
+
def _parse_addr(addr: str) -> tuple[str, list[str]]:
|
|
751
|
+
sep = next((s for s in SEPS if s in addr), "/")
|
|
752
|
+
return sep, addr.split(sep)
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def _fits(writer: list[str], reader: list[str], anchored: bool) -> int:
|
|
756
|
+
"""How many fixed parts of a reader's shape sit at the same places in a writer's shape, at the best alignment
|
|
757
|
+
where no fixed part disagrees. 0 when none fits."""
|
|
758
|
+
best = 0
|
|
759
|
+
offsets = [0] if anchored else range(0, max(1, len(writer) - 1))
|
|
760
|
+
for k in offsets:
|
|
761
|
+
hits, bad = 0, False
|
|
762
|
+
for i, r in enumerate(reader):
|
|
763
|
+
if i + k >= len(writer):
|
|
764
|
+
break
|
|
765
|
+
w = writer[i + k]
|
|
766
|
+
if r == HOLE or w == HOLE:
|
|
767
|
+
continue
|
|
768
|
+
if r == w:
|
|
769
|
+
hits += WORD.match(r) is not None
|
|
770
|
+
else:
|
|
771
|
+
bad = True
|
|
772
|
+
break
|
|
773
|
+
if not bad:
|
|
774
|
+
best = max(best, hits)
|
|
775
|
+
return best
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def _fits_index(writer: list[str], at: dict[int, set], offset: Optional[int]) -> tuple[int, int]:
|
|
779
|
+
"""(hits, offset) for a reader that compares a split key's pieces by position: the writer's part at position
|
|
780
|
+
p + offset must be one of the words compared at p."""
|
|
781
|
+
best = (0, -1)
|
|
782
|
+
offsets = [offset] if offset is not None else range(0, len(writer))
|
|
783
|
+
for k in offsets:
|
|
784
|
+
hits, bad = 0, False
|
|
785
|
+
for p, words in at.items():
|
|
786
|
+
if p + k >= len(writer) or writer[p + k] == HOLE:
|
|
787
|
+
continue
|
|
788
|
+
if writer[p + k] in words:
|
|
789
|
+
hits += 1
|
|
790
|
+
else:
|
|
791
|
+
bad = True
|
|
792
|
+
break
|
|
793
|
+
if not bad and hits > best[0]:
|
|
794
|
+
best = (hits, k)
|
|
795
|
+
return best
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def _in_tests(ix, nid: str) -> bool:
|
|
799
|
+
from .channels import _test_path
|
|
800
|
+
n = ix.nodes.get(nid)
|
|
801
|
+
cur = n
|
|
802
|
+
while cur is not None and cur.kind in ("callable", "test"):
|
|
803
|
+
if cur.kind == "test" or cur.attrs.get("is_test") or cur.attrs.get("is_fixture"):
|
|
804
|
+
return True
|
|
805
|
+
cur = ix.nodes.get(cur.parent_id)
|
|
806
|
+
return n is not None and bool(n.path) and _test_path(n.path)
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
def resolve(ix) -> None:
|
|
810
|
+
"""Link each key writer to the readers of its shape, across the workspace (one repository at a time)."""
|
|
811
|
+
st = ix.channel_stats["format"]
|
|
812
|
+
writers, readers = [], []
|
|
813
|
+
for fid, res in ix.results.items():
|
|
814
|
+
for e in res.endpoints:
|
|
815
|
+
if e.channel != "format" or e.src_id not in ix.nodes:
|
|
816
|
+
continue
|
|
817
|
+
(writers if e.role == "write" else readers).append(e)
|
|
818
|
+
# A hole that calls another key builder takes that builder's shape: `${dialogueNodeKey(d, n)}/text`.
|
|
819
|
+
by_name: dict[tuple, list] = defaultdict(list)
|
|
820
|
+
for e in writers:
|
|
821
|
+
n = ix.nodes[e.src_id]
|
|
822
|
+
by_name[(e.src_id.split(":", 1)[0], n.name)].append(e)
|
|
823
|
+
done: dict[int, Optional[tuple]] = {}
|
|
824
|
+
|
|
825
|
+
def expand(e, depth=0):
|
|
826
|
+
if id(e) in done:
|
|
827
|
+
return done[id(e)]
|
|
828
|
+
done[id(e)] = None
|
|
829
|
+
sep, parts = _parse_addr(e.address)
|
|
830
|
+
out = []
|
|
831
|
+
for p in parts:
|
|
832
|
+
if p.startswith("@"):
|
|
833
|
+
cands = [w for w in by_name.get((e.src_id.split(":", 1)[0], p[1:]), []) if w is not e]
|
|
834
|
+
same = [w for w in cands if ix.file_of.get(w.src_id) == ix.file_of.get(e.src_id)]
|
|
835
|
+
cands = same or cands
|
|
836
|
+
shapes = {expand(w, depth + 1) for w in cands} if depth < 4 else set()
|
|
837
|
+
shapes.discard(None)
|
|
838
|
+
if len(shapes) != 1 or len({w.src_id for w in cands}) != 1:
|
|
839
|
+
if not out:
|
|
840
|
+
return None # what the key starts with is not known
|
|
841
|
+
out.append(HOLE)
|
|
842
|
+
continue
|
|
843
|
+
s2, p2 = next(iter(shapes))
|
|
844
|
+
if s2 != sep:
|
|
845
|
+
return None
|
|
846
|
+
out.extend(p2)
|
|
847
|
+
else:
|
|
848
|
+
out.append(p)
|
|
849
|
+
got = (sep, tuple(out)) if is_key(out) else None
|
|
850
|
+
done[id(e)] = got
|
|
851
|
+
return got
|
|
852
|
+
|
|
853
|
+
shaped = []
|
|
854
|
+
for e in writers:
|
|
855
|
+
if _in_tests(ix, e.src_id):
|
|
856
|
+
st["written_in_tests"] += 1 # a test writes a sample key to feed the reader; it does not set the format
|
|
857
|
+
continue
|
|
858
|
+
got = expand(e)
|
|
859
|
+
if got is not None:
|
|
860
|
+
shaped.append((e, got[0], list(got[1])))
|
|
861
|
+
st["writers"] = len(shaped)
|
|
862
|
+
st["readers"] = len(readers)
|
|
863
|
+
per_shape = Counter((ix.nodes[e.src_id].id.split(":", 1)[0], sep, tuple(p)) for e, sep, p in shaped)
|
|
864
|
+
pairs = {}
|
|
865
|
+
for r in readers:
|
|
866
|
+
rrepo = r.src_id.split(":", 1)[0]
|
|
867
|
+
if r.method == "index":
|
|
868
|
+
sep = r.address.split(" ", 1)[0][-1]
|
|
869
|
+
at: dict[int, set] = {}
|
|
870
|
+
offset = None
|
|
871
|
+
for lit in r.literals:
|
|
872
|
+
k, _, v = lit.partition("=")
|
|
873
|
+
if k == "offset":
|
|
874
|
+
offset = int(v)
|
|
875
|
+
else:
|
|
876
|
+
at[int(k)] = set(v.split("|"))
|
|
877
|
+
else:
|
|
878
|
+
sep, rparts = _parse_addr(r.address)
|
|
879
|
+
found = [] # (writer, parts, hits, offset)
|
|
880
|
+
for e, wsep, wparts in shaped:
|
|
881
|
+
if wsep != sep or e.src_id.split(":", 1)[0] != rrepo or e.src_id == r.src_id:
|
|
882
|
+
continue
|
|
883
|
+
k = 0
|
|
884
|
+
if r.method == "index":
|
|
885
|
+
hits, k = _fits_index(wparts, at, offset)
|
|
886
|
+
elif r.method == "end":
|
|
887
|
+
hits = _fits(list(reversed(wparts)), list(reversed(rparts)), True)
|
|
888
|
+
else:
|
|
889
|
+
hits = _fits(wparts, rparts, "anchored" in r.literals)
|
|
890
|
+
if hits >= 2:
|
|
891
|
+
found.append((e, wparts, hits, k))
|
|
892
|
+
if r.method == "index" and offset is None and len({k for *_, k in found}) > 1:
|
|
893
|
+
# The pieces come from a parameter, so where they start in the key is not seen. Two formats can share a
|
|
894
|
+
# tail (dialogue/<d>/nodes/<n>/text and nodes/<n>/text): the reader is taken to read the one its own
|
|
895
|
+
# file writes, else the one written in the most files, and not both.
|
|
896
|
+
here = ix.file_of.get(r.src_id)
|
|
897
|
+
own = {k for e, _p, _h, k in found if ix.file_of.get(e.src_id) == here}
|
|
898
|
+
votes = Counter(k for k, _f in {(k, ix.file_of.get(e.src_id)) for e, _p, _h, k in found})
|
|
899
|
+
top = votes.most_common()
|
|
900
|
+
if len(own) == 1:
|
|
901
|
+
keep = own
|
|
902
|
+
elif len(top) > 1 and top[0][1] == top[1][1]:
|
|
903
|
+
st["readers_of_two_formats"] += 1
|
|
904
|
+
keep = set()
|
|
905
|
+
else:
|
|
906
|
+
keep = {top[0][0]}
|
|
907
|
+
found = [f for f in found if f[3] in keep]
|
|
908
|
+
for e, wparts, hits, _k in found:
|
|
909
|
+
key = (e.src_id, r.src_id)
|
|
910
|
+
prev = pairs.get(key)
|
|
911
|
+
if prev is None or hits > prev[1]:
|
|
912
|
+
pairs[key] = (address(sep, wparts), hits, r.method, r.line, e.line)
|
|
913
|
+
fan_in = Counter(r for (_w, r) in pairs)
|
|
914
|
+
for (w, r), (addr, hits, how, rline, wline) in sorted(pairs.items()):
|
|
915
|
+
sep, parts = _parse_addr(addr)
|
|
916
|
+
if per_shape[(w.split(":", 1)[0], sep, tuple(parts))] > TOO_MANY_WRITERS or fan_in[r] > TOO_MANY_READERS:
|
|
917
|
+
st["too_common_to_link"] += 1
|
|
918
|
+
continue
|
|
919
|
+
if w == r or w not in ix.nodes or r not in ix.nodes:
|
|
920
|
+
continue
|
|
921
|
+
ix.edges.append(Edge("communicates", w, r, "heuristic" if hits >= 3 else "guess",
|
|
922
|
+
{"channel": "format", "address": addr, "read_by": how, "line": wline, "read_at": rline}))
|
|
923
|
+
st["links"] += 1
|