|
| 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