import re
from pathlib import Path
import os
ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / 'src'
DOCS = ROOT / 'docs'
OUT = DOCS / 'PLUGINS_AUDIT_PLUGINS.md'
files = list(SRC.rglob('*.rs')) + list(DOCS.rglob('*'))
content = {}
for p in files:
try:
content[str(p.relative_to(ROOT))] = p.read_text(encoding='utf-8', errors='ignore')
except Exception:
content[str(p.relative_to(ROOT))] = ''
plugin_files = sorted((SRC / 'plugins').rglob('*.rs'))
pub_re = re.compile(r"pub\s+(?:struct|enum)\s+(\w+)")
derive_re = re.compile(r"derive\s*\(\s*[^)]*\bRegisterPlugin\b[^)]*\)")
exec_derive_re = re.compile(r"derive\s*\(\s*[^)]*\bRegisterExecPlugin\b[^)]*\)")
factory_force_re = re.compile(r"Lazy::force\(&crate::plugins::([\w:]+)::([A-Z0-9_]+)\)")
def classify_path(rel_path, file_text):
if rel_path.startswith('docs/') or rel_path.lower().endswith('.md'):
return 'doc'
if '/tests/' in rel_path or rel_path.startswith('tests/'):
return 'test'
if '#[cfg(test)]' in file_text or 'mod tests' in file_text:
return 'test'
return 'runtime'
all_text = '\n'.join(content.values())
rows = []
for pf in plugin_files:
rel = str(pf.relative_to(ROOT))
text = content.get(rel, '')
exports = pub_re.findall(text)
registered = False
if derive_re.search(text) or exec_derive_re.search(text):
registered = True
module_path = str(pf.relative_to(SRC)).replace('.rs','').replace(os.sep, '::')
for m in factory_force_re.findall(all_text):
if module_path in m[0]:
registered = True
break
runtime_refs = set()
test_refs = set()
doc_refs = set()
total_refs = 0
symbols = exports if exports else [pf.stem]
for sym in symbols:
wre = re.compile(r"\b" + re.escape(sym) + r"\b")
for rel2, txt in content.items():
count = len(wre.findall(txt))
if count == 0:
continue
total_refs += count
cat = classify_path(rel2, txt)
if cat == 'runtime':
runtime_refs.add(rel2)
elif cat == 'test':
test_refs.add(rel2)
elif cat == 'doc':
doc_refs.add(rel2)
cats = set()
if runtime_refs: cats.add('runtime')
if test_refs: cats.add('tests')
if doc_refs: cats.add('docs')
if not cats:
classification = 'unused'
elif len(cats) == 1:
classification = next(iter(cats))
else:
classification = 'mixed'
rows.append({
'file': rel,
'exports': ', '.join(exports) if exports else '-',
'registered': 'yes' if registered else 'no',
'runtime_refs': len(runtime_refs),
'test_refs': len(test_refs),
'doc_refs': len(doc_refs),
'total_refs': total_refs,
'classification': classification,
})
lines = []
lines.append('# Plugins Audit (plugins/ only)')
lines.append('Generated by scripts/generate_plugins_audit_plugins.py')
lines.append('')
lines.append('| File | Exports | Registered? | Runtime refs | Test refs | Doc refs | Total refs | Classification |')
lines.append('|------|---------|-------------:|-------------:|---------:|---------:|-----------:|----------------:|')
for r in rows:
lines.append(f"| `{r['file']}` | {r['exports']} | {r['registered']} | {r['runtime_refs']} | {r['test_refs']} | {r['doc_refs']} | {r['total_refs']} | {r['classification']} |")
OUT.write_text('\n'.join(lines), encoding='utf-8')
print('Wrote', OUT)