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/grade.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Grade the call links an adapter found against a compiler's, read from a SCIP index.
|
|
2
|
+
|
|
3
|
+
A call in the compiler's index is a reference, at a place in the source where the name is followed
|
|
4
|
+
by `(` or opens a JSX tag, to a symbol whose definition lands on a function on the map. Both sides
|
|
5
|
+
are reduced to (calling function, called function) pairs and compared:
|
|
6
|
+
|
|
7
|
+
- precision: of the links the adapter made, the share the compiler also makes;
|
|
8
|
+
- recall: of the compiler's links, the share the adapter found.
|
|
9
|
+
|
|
10
|
+
Only files the compiler indexed count, and only links whose both ends are on the map, so a function
|
|
11
|
+
the adapter did not declare at all shows up as missed recall, not as a wrong link.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import time
|
|
17
|
+
from collections import Counter, defaultdict
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from . import exact, store
|
|
22
|
+
from .indexer import Indexer, source_lines
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
CTOR_NAMES = ("constructor", ".ctor", "__init__", "new", "init")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def compiler_sites(ix, scip_path: str, prefix: str = "") -> tuple[dict, set, set]:
|
|
29
|
+
"""What the compiler says at each call site.
|
|
30
|
+
|
|
31
|
+
Returns (sites, files, mentions): sites maps (calling function, line, name) to the called function's
|
|
32
|
+
id on the map, or to None when the compiler's target is not a function on the map (an interface
|
|
33
|
+
member, a library); files are the repo paths it indexed; mentions are (function, function) pairs
|
|
34
|
+
where one names the other without calling it."""
|
|
35
|
+
from . import scip_pb2
|
|
36
|
+
index = scip_pb2.Index()
|
|
37
|
+
index.ParseFromString(Path(scip_path).read_bytes())
|
|
38
|
+
loc = exact._Locator(ix)
|
|
39
|
+
pre = prefix.strip("/") + "/" if prefix.strip("/") else ""
|
|
40
|
+
types_at = defaultdict(list)
|
|
41
|
+
for n in ix.nodes.values():
|
|
42
|
+
if n.kind == "type" and n.path and n.span_start:
|
|
43
|
+
types_at[n.path].append(n)
|
|
44
|
+
members = defaultdict(list)
|
|
45
|
+
for n in ix.nodes.values():
|
|
46
|
+
if n.kind == "callable":
|
|
47
|
+
members[n.parent_id].append(n)
|
|
48
|
+
definition = {}
|
|
49
|
+
for doc in index.documents:
|
|
50
|
+
for occ in doc.occurrences:
|
|
51
|
+
if occ.symbol_roles & 1 and not occ.symbol.startswith("local ") and exact.occ_range(occ):
|
|
52
|
+
definition[occ.symbol] = (pre + doc.relative_path, exact.occ_range(occ)[0] + 1)
|
|
53
|
+
sites, files, mentions = {}, set(), set()
|
|
54
|
+
for doc in index.documents:
|
|
55
|
+
path = pre + doc.relative_path
|
|
56
|
+
files.add(path)
|
|
57
|
+
src = ix.root / path
|
|
58
|
+
try:
|
|
59
|
+
lines = source_lines(src) if src.is_file() else []
|
|
60
|
+
except OSError:
|
|
61
|
+
lines = []
|
|
62
|
+
for occ in doc.occurrences:
|
|
63
|
+
sym = occ.symbol
|
|
64
|
+
if occ.symbol_roles & 1:
|
|
65
|
+
continue
|
|
66
|
+
row, col, end = exact.occ_range(occ) or (len(lines), 0, 0)
|
|
67
|
+
if row >= len(lines):
|
|
68
|
+
continue
|
|
69
|
+
col, end = (exact._chars(lines[row], c, doc.position_encoding) for c in (col, end))
|
|
70
|
+
if sym.startswith("local "):
|
|
71
|
+
# A local the compiler could not tie to a declaration (often an import it failed to follow):
|
|
72
|
+
# a call through it is not judged either way.
|
|
73
|
+
src_node = loc.caller(path, row + 1)
|
|
74
|
+
word = lines[row][col:end].strip()
|
|
75
|
+
if src_node is not None and lines[row][end:].lstrip().startswith("("):
|
|
76
|
+
sites.setdefault((src_node.id, row + 1, word), None)
|
|
77
|
+
continue
|
|
78
|
+
text = lines[row]
|
|
79
|
+
after, before = text[end:].lstrip(), text[:col].rstrip()
|
|
80
|
+
called = after.startswith("(") or after[1:].lstrip()[:1] in ("(", "[", "{") and after[:1] == "!" or (after.startswith("<") and "(" in after) or before.endswith(("<", "new"))
|
|
81
|
+
if before.endswith("def") and after.startswith("("):
|
|
82
|
+
continue # `def f(`: an overload's implementation or a property setter names the symbol again
|
|
83
|
+
word = text[col:end].strip()
|
|
84
|
+
name = exact._scip_name(sym).strip("<>")
|
|
85
|
+
src_node = loc.caller(path, row + 1) if called else loc.enclosing(path, row + 1)
|
|
86
|
+
if src_node is None:
|
|
87
|
+
continue
|
|
88
|
+
dst = None
|
|
89
|
+
macro = after.startswith("!")
|
|
90
|
+
if macro:
|
|
91
|
+
word += "!" # a Rust macro: the map names it with its bang
|
|
92
|
+
if sym in definition and macro == name.endswith("!"):
|
|
93
|
+
tf, tl = definition[sym]
|
|
94
|
+
dst = loc.target(tf, tl, name, ("callable",))
|
|
95
|
+
if dst is None and called and (name in CTOR_NAMES or word[:1].isupper()):
|
|
96
|
+
# `new Foo()` points at the class or at its constructor: either way, the constructor.
|
|
97
|
+
t = next((n for n in types_at.get(tf, ()) if n.name in (word, name) and n.span_start <= tl <= (n.span_end or n.span_start)), None)
|
|
98
|
+
ctor = [m for m in members.get(t.id, ()) if m.name in CTOR_NAMES] if t is not None else []
|
|
99
|
+
dst = ctor[0] if ctor else None
|
|
100
|
+
if not called:
|
|
101
|
+
if dst is not None and src_node.id != dst.id and not before.lstrip().startswith(("import", "export", "from")):
|
|
102
|
+
mentions.add((src_node.id, dst.id))
|
|
103
|
+
continue
|
|
104
|
+
key = (src_node.id, row + 1, word or name)
|
|
105
|
+
if dst is not None and dst.id != src_node.id:
|
|
106
|
+
sites[key] = dst.id
|
|
107
|
+
else:
|
|
108
|
+
sites.setdefault(key, None)
|
|
109
|
+
return sites, files, mentions
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def roslyn_sites(ix) -> tuple[dict, set, set]:
|
|
113
|
+
"""The same as compiler_sites, from the C# compiler run directly (no SCIP indexer needed)."""
|
|
114
|
+
records, info = exact.roslyn(ix)
|
|
115
|
+
loc = exact._Locator(ix)
|
|
116
|
+
sites, files = {}, set()
|
|
117
|
+
for r in records:
|
|
118
|
+
if r["k"] == "file":
|
|
119
|
+
files.add(r["f"])
|
|
120
|
+
continue
|
|
121
|
+
if r["k"] != "call":
|
|
122
|
+
continue
|
|
123
|
+
src = loc.caller(r["f"], r["l"])
|
|
124
|
+
if src is None:
|
|
125
|
+
continue
|
|
126
|
+
name = r.get("tn") or r["n"]
|
|
127
|
+
dst = None
|
|
128
|
+
if r["s"] in ("ok", "candidate") and r.get("tf"):
|
|
129
|
+
for nm in ((name, r["n"]) if name != ".ctor" else (".ctor", r["n"], "constructor")):
|
|
130
|
+
dst = loc.target(r["tf"], r["tl"], nm, ("callable",))
|
|
131
|
+
if dst is not None:
|
|
132
|
+
break
|
|
133
|
+
key = (src.id, r["l"], ix.nodes[dst.parent_id].name if dst is not None and name == ".ctor" and dst.parent_id in ix.nodes else r["n"])
|
|
134
|
+
if dst is not None and dst.id != src.id:
|
|
135
|
+
sites[key] = dst.id
|
|
136
|
+
else:
|
|
137
|
+
sites.setdefault(key, None)
|
|
138
|
+
return sites, files, set()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def grade(root: str, scip_path: str, prefix: str = "", repo_id: Optional[str] = None, db: Optional[str] = None) -> dict:
|
|
142
|
+
root_p = Path(root).resolve()
|
|
143
|
+
ix = Indexer(root_p, repo_id)
|
|
144
|
+
started = time.perf_counter()
|
|
145
|
+
db = db or str(root_p / ".leyline" / "grade.db")
|
|
146
|
+
Path(db).parent.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
con = store.connect(db)
|
|
148
|
+
ix.run(con)
|
|
149
|
+
con.close()
|
|
150
|
+
seconds = round(time.perf_counter() - started, 2)
|
|
151
|
+
sites, files, mentions = roslyn_sites(ix) if scip_path == "roslyn" else compiler_sites(ix, scip_path, prefix)
|
|
152
|
+
on_map = {n.id for n in ix.nodes.values() if n.kind in ("callable", "test")}
|
|
153
|
+
truth = {(k[0], v) for k, v in sites.items() if v and k[0] in on_map and v in on_map}
|
|
154
|
+
by_line = defaultdict(dict)
|
|
155
|
+
for (s_id, line, word), dst in sites.items():
|
|
156
|
+
by_line[(s_id, line)][word] = dst
|
|
157
|
+
judged, silent, offmap = defaultdict(set), defaultdict(set), 0
|
|
158
|
+
refs = set()
|
|
159
|
+
for src, dst, dispatch, precision, line in ix.calls:
|
|
160
|
+
node = ix.nodes.get(src)
|
|
161
|
+
if node is None or node.path not in files or src == dst:
|
|
162
|
+
continue
|
|
163
|
+
if dispatch in ("fixture", "registers"):
|
|
164
|
+
continue # pytest handing a test its fixture, or a router given a route's handler, is not a call written there
|
|
165
|
+
if dispatch == "reference":
|
|
166
|
+
refs.add((src, dst))
|
|
167
|
+
continue
|
|
168
|
+
name = ix.nodes[dst].name
|
|
169
|
+
verdicts = by_line.get((src, line), {})
|
|
170
|
+
if name in CTOR_NAMES:
|
|
171
|
+
# `new Foo()` is recorded under Foo; Rust's `Foo::new()` is an ordinary call recorded under new. Foo
|
|
172
|
+
# first: in `new Foo(new Bar { })` the compiler also records Bar's implicit constructor as .ctor.
|
|
173
|
+
owner = ix.nodes[ix.nodes[dst].parent_id].name if ix.nodes[dst].parent_id in ix.nodes else name
|
|
174
|
+
name = owner if owner in verdicts or name not in verdicts else name
|
|
175
|
+
if name in verdicts:
|
|
176
|
+
if verdicts[name] is None:
|
|
177
|
+
offmap += 1 # the compiler resolved it to something that is not a function on the map: not judged
|
|
178
|
+
continue
|
|
179
|
+
judged[precision].add((src, dst, verdicts[name] == dst))
|
|
180
|
+
else:
|
|
181
|
+
silent[precision].add((src, dst)) # the compiler saw no call of that name there
|
|
182
|
+
every = {(s_, d) for v in judged.values() for s_, d, _ in v} | {p for v in silent.values() for p in v}
|
|
183
|
+
right = {(s_, d) for v in judged.values() for s_, d, ok in v if ok}
|
|
184
|
+
out = {"seconds_to_index": seconds, "files_compared": len(files),
|
|
185
|
+
"functions_on_map": len(on_map), "compiler_links": len(truth), "adapter_links": len(every),
|
|
186
|
+
# Precision counts only links at sites the compiler resolved. A link at a site where the compiler
|
|
187
|
+
# recorded no call of that name is listed apart: there the compiler could not see either.
|
|
188
|
+
"precision": round(len(right) / max(1, sum(len(v) for v in judged.values())), 3) if judged else None,
|
|
189
|
+
"compiler_silent": sum(len(v) for v in silent.values()),
|
|
190
|
+
"recall": round(len(right & truth) / len(truth), 3) if truth else None,
|
|
191
|
+
"by_kind": {k: {"judged": len(judged[k]), "right": sum(ok for *_, ok in judged[k]), "compiler_silent": len(silent[k])}
|
|
192
|
+
for k in sorted(set(judged) | set(silent))},
|
|
193
|
+
"not_judged_compiler_target_off_map": offmap,
|
|
194
|
+
"references": {"links": len(refs), "precision": round(len(refs & (mentions | truth)) / len(refs), 3) if refs else None},
|
|
195
|
+
"adapters": sorted(k for k in ix.stats if k.startswith(("tree-sitter", "generic")))}
|
|
196
|
+
wrong_pairs = {(s_, d) for v in judged.values() for s_, d, ok in v if not ok} | {p for v in silent.values() for p in v}
|
|
197
|
+
every = wrong_pairs | right
|
|
198
|
+
missed = Counter()
|
|
199
|
+
for s, d in truth - right:
|
|
200
|
+
missed[ix.nodes[d].name] += 1
|
|
201
|
+
wrong = Counter()
|
|
202
|
+
for s, d in wrong_pairs:
|
|
203
|
+
wrong[ix.nodes[d].name] += 1
|
|
204
|
+
out["most_missed"] = missed.most_common(12)
|
|
205
|
+
out["most_wrong"] = wrong.most_common(12)
|
|
206
|
+
out["_samples"] = {"missed": sorted(truth - right)[:400], "wrong": sorted(wrong_pairs)[:400]}
|
|
207
|
+
return out
|