Skip to content

Commit 80383d5

Browse files
Add new iReport plugin for exporting metrics for certain entities [autosync]
1 parent 2327c89 commit 80383d5

1 file changed

Lines changed: 223 additions & 0 deletions

File tree

IReport/export_entity_metrics.upy

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# Export Entity Metrics - Interactive Report Plugin
2+
# Exports metrics for selected C++ entity kinds with optional CSV output.
3+
# Install by dragging this file into Understand and enabling under Tools -> Plugin Manager.
4+
#
5+
# NOTE: Default entity kinds target the most common types in C++ projects.
6+
# To add more kinds (e.g. Typedefs, Macros, Variables), add an entry
7+
# to ENTITY_KINDS and the choices list in init().
8+
9+
import understand
10+
import csv
11+
import os
12+
import json
13+
14+
def name():
15+
"""Required, the name of the iReport."""
16+
return "Export Entity Metrics"
17+
18+
def description():
19+
return '''Export metrics for specific entity kinds as an interactive table or CSV file.
20+
21+
<p>Select one or more entity kinds (Classes, Structs, Functions, Methods,
22+
Files, Enums, Namespaces) and optionally include Unknown or Unresolved
23+
entities. Results are displayed as a sortable, filterable table inside
24+
Understand.</p>
25+
26+
<p>The default entity kinds target the most common types in C++ projects.
27+
To add additional kinds (e.g. Typedefs, Macros, Variables), add an entry
28+
to <code>ENTITY_KINDS</code> and the checkbox choices in
29+
<code>init()</code>.</p>
30+
31+
<p>Enable <b>Export to CSV</b> and use the file selector to choose where
32+
the CSV is saved.</p>
33+
'''
34+
35+
def tags():
36+
return [
37+
'Target: Project',
38+
'Language: C',
39+
'Language: C++',
40+
'Language: Any',
41+
]
42+
43+
def test_global(db):
44+
"""This is a project-level report."""
45+
return True
46+
47+
def test_entity(ent):
48+
"""Not an entity-level report."""
49+
return False
50+
51+
def test_architecture(arch):
52+
"""Not an architecture-level report."""
53+
return False
54+
55+
def init(report, target):
56+
"""Define report options."""
57+
opts = report.options()
58+
59+
# ── Entity Kinds ──
60+
opts.label("kinds_label", "Entity Kinds")
61+
opts.checkbox_vert("entity_kinds", "Select which entity kinds to include",
62+
["Classes", "Structs", "Functions", "Methods", "Files", "Enums", "Namespaces"],
63+
["Classes"]) # Classes checked by default
64+
65+
# ── Filters ──
66+
opts.label("filters_label", "Filters")
67+
opts.checkbox("include_unknown", "Include Unknown Entities", False)
68+
opts.checkbox("include_unresolved", "Include Unresolved Entities", False)
69+
70+
# ── Output ──
71+
opts.label("output_label", "Output")
72+
opts.checkbox("export_csv", "Export to CSV", False)
73+
opts.file("csv_path", "CSV Save Location", "entity_metrics_export.csv")
74+
75+
76+
# ─────────────────────────────────────────────────────────────────────
77+
# Entity kind registry — maps checkbox labels to Understand kind strings.
78+
# To add a new kind, append an entry here and add it to the
79+
# checkbox_vert choices list in init().
80+
# ─────────────────────────────────────────────────────────────────────
81+
ENTITY_KINDS = {
82+
"Classes": "class",
83+
"Structs": "struct",
84+
"Functions": "function",
85+
"Methods": "method",
86+
"Files": "file",
87+
"Enums": "enum",
88+
"Namespaces": "namespace",
89+
}
90+
91+
92+
def generate(report, target):
93+
"""Required, generate the report."""
94+
db = report.db()
95+
opts = report.options()
96+
97+
# Get selected entity kinds from the checkbox group
98+
selected_kinds = opts.lookup("entity_kinds")
99+
if not selected_kinds:
100+
report.print("No entity kinds selected. Check at least one Entity Kind option above.")
101+
return
102+
103+
# Map selected labels to Understand kind filter strings
104+
kind_parts = []
105+
for kind_label in selected_kinds:
106+
if kind_label in ENTITY_KINDS:
107+
kind_parts.append(ENTITY_KINDS[kind_label])
108+
109+
if not kind_parts:
110+
report.print("No valid entity kinds selected.")
111+
return
112+
113+
# Build the kindstring: e.g. "class,function ~unknown ~unresolved"
114+
kindfilter = ",".join(kind_parts)
115+
116+
# Append qualifier exclusions unless toggled on
117+
if not opts.lookup("include_unknown"):
118+
kindfilter += " ~unknown"
119+
if not opts.lookup("include_unresolved"):
120+
kindfilter += " ~unresolved"
121+
122+
# Query entities
123+
entities = db.ents(kindfilter)
124+
if not entities:
125+
report.print("No entities found matching: '{}'\n".format(kindfilter))
126+
return
127+
128+
# Collect all unique metric names across matched entities
129+
all_metric_names = set()
130+
for ent in entities:
131+
all_metric_names.update(ent.metrics())
132+
all_metric_names = sorted(all_metric_names)
133+
134+
if not all_metric_names:
135+
report.print("No metrics available for the selected entity kinds.\n")
136+
return
137+
138+
# Route to CSV or table output
139+
if opts.lookup("export_csv"):
140+
csv_path = opts.lookup("csv_path")
141+
if not csv_path:
142+
csv_path = "entity_metrics_export.csv"
143+
export_csv(report, db, entities, all_metric_names, kindfilter, csv_path)
144+
else:
145+
render_table(report, entities, all_metric_names)
146+
147+
148+
def export_csv(report, db, entities, metric_names, kindfilter, csv_path):
149+
"""Write metrics to a CSV file at the user-specified path."""
150+
# If the path isn't absolute, save it next to the project
151+
if not os.path.isabs(csv_path):
152+
db_path = db.name()
153+
if db_path:
154+
csv_path = os.path.join(os.path.dirname(db_path), csv_path)
155+
156+
try:
157+
with open(csv_path, "w", newline="") as f:
158+
writer = csv.writer(f)
159+
writer.writerow(["Entity", "Kind"] + metric_names)
160+
for ent in entities:
161+
values = ent.metric(metric_names)
162+
row = [ent.longname(), str(ent.kind())]
163+
for m in metric_names:
164+
val = values.get(m) if isinstance(values, dict) else None
165+
row.append(val if val is not None else "")
166+
writer.writerow(row)
167+
168+
report.bold()
169+
report.print("CSV Export Complete\n")
170+
report.bold()
171+
report.print("\n")
172+
173+
report.print("File: ")
174+
report.bold()
175+
report.print(csv_path)
176+
report.bold()
177+
report.print("\n")
178+
179+
report.print("Entities: {}\n".format(len(entities)))
180+
report.print("Metrics: {}\n".format(len(metric_names)))
181+
report.print("Kind filter: {}\n".format(kindfilter))
182+
183+
except Exception as e:
184+
report.print("Error writing CSV: {}\n".format(str(e)))
185+
186+
187+
def render_table(report, entities, metric_names):
188+
"""Render an interactive, sortable table inside Understand."""
189+
# Build column definitions for the interactive table
190+
columns = [
191+
{"name": "Entity", "filtertype": "string"},
192+
{"name": "Kind", "filtertype": "string"},
193+
]
194+
for m in metric_names:
195+
columns.append({
196+
"name": m,
197+
"filtertype": "numeric",
198+
})
199+
200+
report.print("{} entities, {} metrics\n\n".format(len(entities), len(metric_names)))
201+
report.table(json.dumps(columns))
202+
203+
for ent in entities:
204+
values = ent.metric(metric_names)
205+
206+
# Entity name cell — linked to the entity for navigation
207+
report.tablecell()
208+
report.entity(ent)
209+
report.print(ent.longname())
210+
report.entity()
211+
212+
# Kind cell
213+
report.tablecell()
214+
report.print(str(ent.kind()))
215+
216+
# Metric value cells
217+
for m in metric_names:
218+
report.tablecell()
219+
val = values.get(m) if isinstance(values, dict) else None
220+
if val is not None:
221+
report.print(str(val))
222+
223+
report.table()

0 commit comments

Comments
 (0)