Standalone plugin with one skill: when to use orbit and when grep, a mandatory preflight for stale indexes and blind regions (cfg(test), features, macros, dyn dispatch), a scripted review pass over isolated worktree DBs, and a JSONL feedback log for mining real usage later. Registered in the marketplace as a prototype.
108 lines
6.3 KiB
Python
Executable File
108 lines
6.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Recipe 3: trait-impl coverage for traits touched by a diff (or named with --trait).
|
|
|
|
Usage: traitcov.py <env-file> <defdiff.json> [--trait Name ...] [--all-traits]
|
|
For each trait: its methods at tip (+/- vs base), implementors from three sources, and per
|
|
implementor which trait methods it defines (and whether the diff touched them):
|
|
E = EXTENDS edge (orbit; only when `impl Trait for Type` sits in the type's own file)
|
|
G = `git grep 'impl … Trait … for Type'` at tip (text; sees cfg-gated/test impls too)
|
|
H = method-name overlap heuristic (orbit; >= 60% of trait methods defined on the type)
|
|
Implementors only in G are cross-file impls, impls on external types, or cfg-gated/test impls.
|
|
"""
|
|
import collections, json, re, subprocess, sys
|
|
sys.path.insert(0, __file__.rsplit('/', 1)[0])
|
|
from ob import load_env, q, git
|
|
from regions import cfg_regions, region_of
|
|
|
|
env = load_env(sys.argv[1]); dd = json.load(open(sys.argv[2]))
|
|
DB, BW, TW, REPO, B, T = env['DB'], env['BASE_WT'], env['TIP_WT'], env['REPO'], env['BASE_SHA'], env['TIP_SHA']
|
|
names = [sys.argv[i + 1] for i, a in enumerate(sys.argv) if a == '--trait']
|
|
|
|
D = {}
|
|
for side, wt in (('tip', TW), ('base', BW)):
|
|
D[side] = q("SELECT id, fqn, name, definition_type k, file_path f, start_line s, end_line e FROM gl_definition WHERE file_path LIKE '%.rs'", repo=wt, db=DB)
|
|
traits = {d['fqn']: d for d in D['tip'] if d['k'] == 'Trait'}
|
|
btraits = {d['fqn']: d for d in D['base'] if d['k'] == 'Trait'}
|
|
touched = set(dd['touched_tip'])
|
|
if '--all-traits' in sys.argv: sel = set(traits)
|
|
elif names: sel = {f for f, d in traits.items() if d['name'] in names}
|
|
else:
|
|
sel = set()
|
|
for r in dd['rows']:
|
|
par = r['fqn'].rsplit('::', 1)[0]
|
|
if r['k'] == 'Trait' and r['fqn'] in traits: sel.add(r['fqn'])
|
|
if par in traits and r['op'] != '~' or par in traits and r['op'] == '~': sel.add(par)
|
|
for r in dd['rows']:
|
|
if r['k'] == 'Trait' and r['op'] == '-': print(f"- Trait {r['fqn']} REMOVED — check leftover impls with: git grep -n 'impl .*{r['name']}' {T[:8]}")
|
|
|
|
ext = collections.defaultdict(set)
|
|
for e in q("SELECT s.fqn src, t.fqn tgt FROM gl_edge e JOIN gl_definition s ON s.id=e.source_id JOIN gl_definition t ON t.id=e.target_id WHERE e.relationship_kind='EXTENDS'", repo=TW, db=DB):
|
|
ext[e['tgt']].add(e['src'])
|
|
methods_of = collections.defaultdict(dict)
|
|
for d in D['tip']:
|
|
if d['k'] in ('Method', 'AssociatedFunction', 'Constant', 'TypeAlias'):
|
|
methods_of[d['fqn'].rsplit('::', 1)[0]][d['name']] = d
|
|
bmethods_of = collections.defaultdict(dict)
|
|
for d in D['base']:
|
|
if d['k'] in ('Method', 'AssociatedFunction'):
|
|
bmethods_of[d['fqn'].rsplit('::', 1)[0]][d['name']] = d
|
|
types_by_name = collections.defaultdict(list)
|
|
for d in D['tip']:
|
|
if d['k'] in ('Struct', 'Enum', 'TypeAlias', 'Trait'): types_by_name[d['name']].append(d)
|
|
_src = {}
|
|
def regions(f):
|
|
if f not in _src:
|
|
try: _src[f] = cfg_regions(open(f"{TW}/{f}", encoding='utf-8', errors='replace').read().split('\n'))
|
|
except FileNotFoundError: _src[f] = []
|
|
return _src[f]
|
|
|
|
stats = collections.Counter()
|
|
for tf in sorted(sel):
|
|
t = traits[tf]; tm = methods_of.get(tf, {}); bm = bmethods_of.get(tf, {})
|
|
added = sorted(set(tm) - set(bm)); removed = sorted(set(bm) - set(tm))
|
|
sigchg = sorted(r['name'] for r in dd['rows'] if r['op'] == 'S' and r['fqn'].rsplit('::', 1)[0] == tf)
|
|
print(f"Trait {tf} [{t['f']}:{t['s']}] methods={len(tm)}" + (f" +{added}" if added else '') + (f" -{removed}" if removed else '') + (f" sig~{sigchg}" if sigchg else ''))
|
|
# G: text impls at tip
|
|
try: g = git(REPO, 'grep', '-nE', rf'^\s*(unsafe\s+)?impl\b.*\b{t["name"]}\b(<[^>]*>)?\s+for\s+', T, '--', '*.rs')
|
|
except subprocess.CalledProcessError: g = ''
|
|
G = {}
|
|
for l in g.splitlines():
|
|
_, f, ln, txt = l.split(':', 3)
|
|
m = re.search(rf'\bfor\s+&?(\'\w+\s+)?(mut\s+)?(dyn\s+)?([\w:]+)', txt)
|
|
if not m: continue
|
|
ty = m.group(4).split('::')[-1]; reg = region_of(regions(f), int(ln))
|
|
G[(ty, f)] = f"cfg-{reg[2]}" if reg else ''
|
|
E = {x.split('::')[-1]: x for x in ext.get(tf, set())}
|
|
# H: overlap heuristic
|
|
H = {}
|
|
if tm:
|
|
for owner, ms in methods_of.items():
|
|
if owner == tf: continue
|
|
ov = len(set(ms) & set(tm))
|
|
if ov >= max(2, 0.6 * len(tm)): H[owner.split('::')[-1]] = owner
|
|
impl_types = sorted(set(E) | {ty for ty, _ in G} | set(H))
|
|
for ty in impl_types:
|
|
src = ''.join(c for c, ok in (('E', ty in E), ('G', any(k[0] == ty for k in G)), ('H', ty in H)) if ok)
|
|
gfiles = [f for (tt, f) in G if tt == ty]; gates = {G[(ty, f)] for f in gfiles if G[(ty, f)]}
|
|
owners = [E.get(ty)] if ty in E else ([H[ty]] if ty in H else [d['fqn'] for d in types_by_name.get(ty, [])])
|
|
owners = [o for o in owners if o]
|
|
have = set(); tch = set()
|
|
for o in owners:
|
|
for n, d in methods_of.get(o, {}).items():
|
|
if n in tm: have.add(n); tch |= {n} if d['id'] in touched else set()
|
|
missing = sorted(set(added) - have) if added else []
|
|
cover = f"defines {len(have)}/{len(tm)}" if owners else "type not indexed (external/gated)"
|
|
flag = ''
|
|
if missing:
|
|
# grep fallback: look for `fn <m>` in the impl files (text; sees gated code)
|
|
txt = {m: any(re.search(rf'\bfn\s+{m}\b', open(f"{TW}/{f}", errors='replace').read()) for f in gfiles) for m in missing}
|
|
ok = [m for m, v in txt.items() if v]; bad = [m for m, v in txt.items() if not v]
|
|
if ok: flag += f" new method(s) {ok} found by text in impl file (orbit can't see it)"
|
|
if bad: flag += f" !! new method(s) {bad} NOT found in impl file (default impl? not updated?)"
|
|
print(f" [{src:3s}] {ty} {cover}{' touched:'+','.join(sorted(tch)) if tch else ''}"
|
|
f"{' impl-in: '+','.join(sorted(set(gfiles)))[:120] if gfiles and 'E' not in src else ''}{' '+','.join(gates) if gates else ''}{flag}")
|
|
stats['G'] += any(k[0] == ty for k in G); stats['E'] += ty in E; stats['H'] += ty in H
|
|
stats['EG'] += (ty in E and any(k[0] == ty for k in G)); stats['HG'] += (ty in H and any(k[0] == ty for k in G))
|
|
stats['H_only'] += (ty in H and not any(k[0] == ty for k in G))
|
|
print(f"# implementors: grep={stats['G']} extends={stats['E']} (∩grep {stats['EG']}) heuristic={stats['H']} (∩grep {stats['HG']}, heuristic-only {stats['H_only']})")
|