]> Repositorios git - classgraph.git/commitdiff
WIP
authorJavier Sagredo <[email protected]>
Wed, 6 May 2026 20:56:21 +0000 (22:56 +0200)
committerJavier Sagredo <[email protected]>
Wed, 6 May 2026 20:56:21 +0000 (22:56 +0200)
README.md
cabal.project
data/viewer.css
data/viewer.html
data/viewer.js
src/Classgraph/Render.hs

index ae67d664d22158eecb1ca6d1cbadd435e090f7d5..4bc5e0f04fb0c7e96e9d90de2bd245e018d98f63 100644 (file)
--- a/README.md
+++ b/README.md
@@ -37,11 +37,12 @@ When you point the plugin at a target package and run the viewer, you get:
   whenever the superclass is mediated by a type family
   (`class Pretty (Norm a) => Foo a`-style). Top-level classes (those no
   other class extends) get a gold border and a `★ top` mark, and are
-  rendered in the topmost row.
+  rendered as the topmost classes.
 - An **instance view** per class, drilled into by double-clicking. Shows
   every instance of that class, the constraints in each instance's
   context, the superclass requirements (and the matching superclass
-  instances when present), and any associated `type instance F …` rows.
+  instances when present), and any associated `type instance F …`
+  declarations.
   When the constraint goes through a type family, you also see the chain
   *focused-instance → family → concrete fam-instance → satisfying
   class-instance*.
@@ -62,7 +63,7 @@ When you point the plugin at a target package and run the viewer, you get:
 - Per-class **instance visibility filter** and per-family **type-instance
   filter** — checkbox lists in the side panel for hiding clutter.
 - A foldable **Help / legend** in the bottom-right of every view.
-- A **Fit** button (and `F` shortcut) to re-frame after hiding rows.
+- A **Fit** button (and `F` shortcut) to re-frame after hiding instances.
 
 ## Quick start: the demo
 
@@ -102,9 +103,8 @@ library
 GHC loads the plugin from `classgraph` in the package set and the cabal
 solver figures everything out. Easiest to set up but **the plugin's
 transitive bounds (`aeson ^>= 2.2`, `ghc ^>= 9.14`, …) become part of
-your build plan** — which can cause a project with tight bounds (e.g.
-ouroboros-consensus, cardano-ledger) to lose a usable build plan. If
-that happens, switch to Option B.
+your build plan** — which can cause a project with tight version bounds
+to lose a usable build plan. If that happens, switch to Option B.
 
 ### Option B — `-fplugin-library` (recommended for real projects)
 
@@ -125,7 +125,7 @@ with `<args>` parsed as a Haskell list-of-strings literal (`["dir=…"]`).
 Concretely, in `cabal.project.local` of your target:
 
 ```
-package ouroboros-consensus
+package my-app
   ghc-options:
     -fplugin-library=/abs/path/to/libHSclassgraph-0.1.0.0-inplace-ghc9.14.1.so;classgraph-0.1.0.0-inplace;Classgraph.Plugin;"[\"dir=.classgraph\"]"
 ```
@@ -142,7 +142,7 @@ correctly-escaped flag, and emits a ready-to-paste cabal stanza:
 
 ```bash
 # From the classgraph checkout (or wherever the script lives):
-./classgraph-plugin-flag.sh --cabal --package ouroboros-consensus \
+./classgraph-plugin-flag.sh --cabal --package my-app \
   >> /path/to/your/project/cabal.project.local
 ```
 
@@ -207,17 +207,17 @@ For an `inplace` (local-checkout cabal-build) the path is
 
 ## Combining dumps from multiple packages
 
-Big projects (consensus + cardano-ledger + …) live in separate cabal
-packages. Build each with the plugin attached, then merge their
-`.classgraph/` directories into a single rendered HTML by repeating
+Big projects often span several cabal packages in the same monorepo or
+neighbouring checkouts. Build each with the plugin attached, then merge
+their `.classgraph/` directories into a single rendered HTML by repeating
 `--input`:
 
 ```bash
 cabal run classgraph-view -- \
-  --input ../cardano-ledger/eras/{shelley,alonzo,babbage,conway}/impl/.classgraph \
-  --input ../cardano-ledger/libs/{cardano-ledger-binary,cardano-ledger-core,cardano-ledger-api}/.classgraph \
-  --input ../ouroboros-consensus/.classgraph \
-  --output cardano.html
+  --input ../pkg-a/.classgraph \
+  --input ../pkg-b/sub/foo/.classgraph \
+  --input ../pkg-c/lib/.classgraph \
+  --output combined.html
 ```
 
 The merger:
@@ -250,11 +250,17 @@ The merger:
 | **Help** (bottom-right) | Foldable legend of every node and edge style |
 
 The instance and family views also have a per-target visibility filter
-in the side panel — checkbox per row, with `Show all` / `Hide all`
+in the side panel — one checkbox per item, with `Show all` / `Hide all`
 buttons and a substring search.
 
 ## Schema, data flow, design notes
 
+For a deeper walkthrough of where every piece of information comes
+from — including how the type-family resolution chains in the instance
+view are computed, and what's deterministic vs. best-effort — see
+[`docs/INTERNALS.md`](docs/INTERNALS.md).
+
+
 The plugin runs in `typeCheckResultAction` (post-typecheck, so all
 classes / instances / family equations are fully resolved). For each
 compiled module it writes one JSON file under the configured directory
@@ -294,8 +300,8 @@ Notable extraction details:
   rather than the class's name span (which is `UnhelpfulSpan` for
   classes loaded from another package's interface file).
 - `fiSrc` for fam-instances uses `coAxBranchSpan` of the underlying
-  `CoAxBranch`, so each row points at its own `data instance` /
-  `type instance` declaration site.
+  `CoAxBranch`, so each type family instance points at its own
+  `data instance` / `type instance` declaration site.
 
 ## Known limitations
 
@@ -306,13 +312,11 @@ Notable extraction details:
   about.** A class defined in a package you didn't build with the plugin
   becomes an `external` stub. The shape of the graph stays correct but
   drilling into such a class shows nothing.
-- **Family resolution chains can stop early** when the relevant fam-instance
-  isn't in your dumps. E.g. for ouroboros-consensus's
-  `HasLedgerTables (LedgerState (ShelleyBlock proto era))`, the
-  superclass `Eq (TxOut blk)` reduces to `Eq (TxOut era)` — itself a
-  family application whose era-specific `type instance TxOut … = …` rows
-  live in `cardano-ledger-shelley` etc. Extract those packages too and
-  the chain will keep going.
+- **Family resolution chains can stop early** when the relevant
+  fam-instance isn't in your dumps. If a class's superclass `Eq (F a)`
+  reduces to `Eq (G b)` whose `type instance G … = …` declarations live in
+  *another* package, the chain visualisation will stop at `G`'s family
+  node. Extract that package too and the chain will keep going.
 - **Pretty-printing is best-effort.** Anything that survives the
   structural converter (`Type` shapes we don't recognise) falls back to
   GHC's `Outputable`, which is much friendlier than it used to be after
@@ -344,17 +348,17 @@ cabal build demo
 cabal run classgraph-view -- --input examples/demo/.classgraph -o demo.html
 ```
 
-To regenerate dumps for a real target (e.g. ouroboros-consensus) using
+To regenerate dumps for a real target (e.g. my-app) using
 `-fplugin-library`:
 
 ```bash
-./classgraph-plugin-flag.sh --cabal --package ouroboros-consensus \
-  >> ../ouroboros-consensus/cabal.project.local
-cd ../ouroboros-consensus
+./classgraph-plugin-flag.sh --cabal --package my-app \
+  >> ../my-app/cabal.project.local
+cd ../my-app
 rm -rf .classgraph
-cabal build lib:ouroboros-consensus
+cabal build lib:my-app
 cd -
-cabal run classgraph-view -- --input ../ouroboros-consensus/.classgraph -o oc.html
+cabal run classgraph-view -- --input ../my-app/.classgraph -o my-app.html
 ```
 
 ## License
index 9e2bf622bad198363f0fc8892a5e091575fe2796..0c4c9385d297acba08a99203182dc95f67b29794 100644 (file)
@@ -1,3 +1,6 @@
+index-state:
+      2026-05-06T18:15:59Z
+
 packages: .
           examples/demo
 
index 62212bbeb9ed2c9f8e139999c37ce1e99ba6cee6..4f8eb05596cbbcb5f8dd0b1111dc3bcaccb311ca 100644 (file)
@@ -136,7 +136,7 @@ body { display: flex; }
   white-space: nowrap;
 }
 .swatch-class    { background: #3b82f6; color: #fff;     border-color: #1d4ed8; }
-.swatch-toplevel { border-color: #f59e0b; border-width: 2px; }
+.swatch-leaf     { border-color: #f59e0b; border-width: 2px; }
 .swatch-family   { background: #fbbf24; color: #3f2d05; border-color: #b45309; }
 .swatch-instance { background: #ecfdf5; color: #065f46; border-color: #10b981; }
 .swatch-faminst  { background: #f5f3ff; color: #5b21b6; border-color: #a78bfa; font-style: italic; }
@@ -199,6 +199,18 @@ body { display: flex; }
 #back-button:hover { background: #f3f4f6; }
 #back-button[hidden] { display: none; }
 
+#orphan-toggle-wrap {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 11px;
+  color: #6b7280;
+  user-select: none;
+  cursor: pointer;
+  white-space: nowrap;
+}
+#orphan-toggle-wrap input { margin: 0; }
+
 #pin-chips {
   display: flex;
   align-items: center;
@@ -263,21 +275,38 @@ body { display: flex; }
 #search-wrap {
   position: relative;
   margin-left: auto;
-  width: 320px;
-  max-width: 50%;
+  width: 460px;
+  max-width: 60%;
+}
+#search-wrap::before {
+  content: '🔍';
+  position: absolute;
+  left: 10px;
+  top: 50%;
+  transform: translateY(-50%);
+  font-size: 14px;
+  pointer-events: none;
+  opacity: 0.55;
 }
 #search {
   width: 100%;
   box-sizing: border-box;
-  padding: 5px 10px;
-  font-size: 13px;
-  border: 1px solid #d1d5db;
-  border-radius: 4px;
+  padding: 9px 12px 9px 34px;
+  font-size: 14px;
+  font-weight: 500;
+  border: 2px solid #cbd5e1;
+  border-radius: 6px;
   outline: none;
   background: #fff;
   color: #111827;
+  transition: border-color 120ms, box-shadow 120ms;
+}
+#search::placeholder { color: #94a3b8; font-weight: 400; }
+#search:hover { border-color: #94a3b8; }
+#search:focus {
+  border-color: #3b82f6;
+  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.18);
 }
-#search:focus { border-color: #3b82f6; box-shadow: 0 0 0 2px rgba(59,130,246,0.15); }
 
 #search-results {
   position: absolute;
index 09aad9d04576f6239a81dc869b445d7ae1c72df0..4f9284c39b3b5607f82d006722dda2368fd521f4 100644 (file)
         <span id="pin-list"></span>
         <button id="pin-clear" title="Show full graph">Clear</button>
       </div>
+      <label id="orphan-toggle-wrap" title="Mark orphan instances with a red dashed border">
+        <input type="checkbox" id="orphan-toggle" />
+        <span>Mark orphans</span>
+      </label>
       <div id="search-wrap">
         <input id="search" type="search" placeholder="Find class or family…  (press / to focus)" autocomplete="off" spellcheck="false" />
         <ul id="search-results" hidden></ul>
         <div class="help-content">
           <h4>Nodes</h4>
           <div class="legend-row"><span class="swatch"><span class="swatch-node swatch-class">Cls</span></span><span>Class defined in this program</span></div>
-          <div class="legend-row"><span class="swatch"><span class="swatch-node swatch-class swatch-toplevel">Cls</span></span><span><strong>Top-level</strong> class (no other class extends it; rendered at the top)</span></div>
+          <div class="legend-row"><span class="swatch"><span class="swatch-node swatch-class swatch-leaf">Cls</span></span><span><strong>Leaf</strong> class (no other class extends it; rendered at the top)</span></div>
           <div class="legend-row"><span class="swatch"><span class="swatch-node swatch-family">Fam</span></span><span>Type family (open / closed / associated)</span></div>
           <div class="legend-row"><span class="swatch"><span class="swatch-node swatch-instance">Inst</span></span><span>Class instance</span></div>
-          <div class="legend-row"><span class="swatch"><span class="swatch-node swatch-faminst">f a=b</span></span><span><code>type instance F … = …</code> row</span></div>
+          <div class="legend-row"><span class="swatch"><span class="swatch-node swatch-faminst">f a=b</span></span><span>Type family instance (<code>type instance F … = …</code>)</span></div>
           <div class="legend-row"><span class="swatch"><span class="swatch-node swatch-external">Ext</span></span><span>External class (referenced but not defined here)</span></div>
           <div class="legend-row"><span class="swatch"><span class="swatch-node swatch-ghost">Cls</span></span><span>Ghost — one-hop neighbour in focus mode (click to pin)</span></div>
           <div class="legend-row"><span class="swatch"><span class="swatch-node swatch-orphan">Inst</span></span><span>Orphan instance (red dashed border)</span></div>
@@ -48,9 +52,9 @@
           <div class="legend-row"><span class="swatch swatch-edge-wrap"><span class="swatch-edge solid amber"></span></span><span>Instance needs a superclass instance (matched locally)</span></div>
           <div class="legend-row"><span class="swatch swatch-edge-wrap"><span class="swatch-edge dashed gray"></span></span><span>Needs a superclass instance (no local match)</span></div>
           <div class="legend-row"><span class="swatch swatch-edge-wrap"><span class="swatch-edge dashed violet-light"></span></span><span>Instance references this type family</span></div>
-          <div class="legend-row"><span class="swatch swatch-edge-wrap"><span class="swatch-edge solid violet"></span></span><span>Family → its type-instance row</span></div>
+          <div class="legend-row"><span class="swatch swatch-edge-wrap"><span class="swatch-edge solid violet"></span></span><span>Family → one of its type family instances</span></div>
           <div class="legend-row"><span class="swatch swatch-edge-wrap"><span class="swatch-edge dotted violet"></span></span><span>Instance's associated <code>type instance</code> RHS</span></div>
-          <div class="legend-row"><span class="swatch swatch-edge-wrap"><span class="swatch-edge dotted teal"></span></span><span>Type-instance row resolves to a class instance (chain)</span></div>
+          <div class="legend-row"><span class="swatch swatch-edge-wrap"><span class="swatch-edge dotted teal"></span></span><span>Type family instance resolves to a class instance (chain)</span></div>
 
           <h4>Controls</h4>
           <div class="help-tip">Click → highlight + side panel</div>
@@ -67,7 +71,7 @@
       <h1>classgraph</h1>
       <p class="hint" id="hint-classes">Search to locate a class. Click any node to inspect it on the right (with <em>Pin</em>/<em>Mute</em> buttons). <strong>Double-click</strong> a class to drill into its instances, or a family (diamond) to see its type instances. Scroll to zoom; drag to pan.</p>
       <p class="hint" id="hint-instance" hidden>Click to highlight; <strong>double-click</strong> a class or family to drill in. The back arrow returns to the hierarchy.</p>
-      <p class="hint" id="hint-family" hidden>Each row is a <code>type instance</code> declaration. <strong>Double-click</strong> the parent class to see its instances.</p>
+      <p class="hint" id="hint-family" hidden>Each entry is a <code>type instance</code> declaration. <strong>Double-click</strong> the parent class to see its instances.</p>
     </header>
     <details id="mute-filter">
       <summary><span id="mute-summary">Muted classes (0)</span></summary>
index ad5868bd13afcb5b40926b21bf11a1d480f7e1e9..666635e62286f164383c42efef5a4994ebc79a5f 100644 (file)
       cy.add(els.map(e => ({ group: e.group, data: Object.assign({}, e.data) })));
     });
     cy.elements().removeClass('highlight').removeClass('dim');
+    applyOrphanMarker();
+  }
+
+  // Toggle orphan border styling. The cy stylesheet only paints
+  // [kind="instance"][?orphan].show-orphan with the red dashed border, so
+  // we add or remove that class on every orphan instance whenever the
+  // checkbox flips.
+  function applyOrphanMarker() {
+    const showOrphans = orphanToggle ? orphanToggle.checked : true;
+    const orphanNodes = cy.nodes('node[kind = "instance"][?orphan]');
+    if (showOrphans) orphanNodes.addClass('show-orphan');
+    else             orphanNodes.removeClass('show-orphan');
   }
 
   function switchToClasses(opts) {
 
     // For every distinct type family referenced anywhere inside `args`,
     // ensure a family node exists, draw a "via family" edge from the
-    // origin node, and lift the family's known type-instance rows into
+    // origin node, and lift the family's known type family instances into
     // the graph so the user can verify the family is actually defined
     // for the relevant types.
     //
             label: 'via ' + fa.qnName,
           }});
         }
-        // Surface only the type-instance rows whose LHS *can* describe
+        // Surface only the type family instances whose LHS *can* describe
         // the focused instance's family-app — anything else is unrelated
         // noise (e.g. `TxIn ByronBlock` while looking at a Shelley
         // instance). Run the relevance check *before* creating the
           'transition-duration': '120ms',
         },
       },
-      // Top-level class — no other class has it as a (direct) superclass.
-      // Visually marked with a gold border so they pop against ordinary
-      // classes; their label also carries an extra "★ top" line baked in
-      // by Render.hs.
+      // Leaf class — no other class has it as a (direct) superclass, so
+      // it's an entry-point in the inheritance graph. Marked with a gold
+      // border so the leaves stand out at the top of the classes view.
       {
-        selector: 'node[kind = "class"][?isTopLevel]',
+        selector: 'node[kind = "class"][?isLeaf]',
         style: {
           'border-color': '#f59e0b',
           'border-width': 2.5,
           'font-family': 'ui-monospace, "SF Mono", Menlo, Consolas, monospace',
         },
       },
+      // Orphan instances get a red dashed border, but only when the
+      // global "Mark orphans" toggle is on. The toggle adds the
+      // `.show-orphan` class to every orphan node in the graph; we only
+      // re-style instances that wear it.
       {
-        selector: 'node[kind = "instance"][?orphan]',
+        selector: 'node[kind = "instance"][?orphan].show-orphan',
         style: {
           'border-color': '#dc2626',
           'border-style': 'dashed',
           'font-size': 9,
         },
       },
-      // Highlight / dim
-      { selector: '.dim',         style: { opacity: 0.12 } },
-      { selector: 'node.highlight', style: { 'border-width': 3, 'border-color': '#f97316' } },
-      { selector: 'edge.highlight', style: { width: 2.5, 'line-color': '#f97316', 'target-arrow-color': '#f97316' } },
+      // Highlight / dim. Highlighted elements keep their original colour
+      // (so the user can still see what kind of edge they're looking at);
+      // we just bump the line/border thickness slightly for emphasis,
+      // and dim everything else.
+      { selector: '.dim',           style: { opacity: 0.12 } },
+      { selector: 'node.highlight', style: { 'border-width': 3 } },
+      { selector: 'edge.highlight', style: { width: 2.4 } },
     ];
   }
 
     switchToClasses();
   });
 
+  // Orphan-marker toggle: when checked, orphan instance nodes get the
+  // red dashed border (the `.show-orphan` class). When unchecked, they
+  // render identically to ordinary instances.
+  const orphanToggle = document.getElementById('orphan-toggle');
+  if (orphanToggle) {
+    orphanToggle.addEventListener('change', applyOrphanMarker);
+  }
+
   // -------------------------------------------------------------------------
   // Side-panel event delegation: toggle Pin/Mute buttons, follow class links.
 
index 35c83a7bb4b76eb6ec99bf4a6c55d1a940934be8..10931ed89207fc546ee18eb6feb481d1722ba546 100644 (file)
@@ -89,35 +89,33 @@ buildGraph pd = CyGraph
   , cyMeta     = Aeson.toJSON pd
   }
   where
-    -- "Top-level" = no other class has this class as a (direct) superclass.
-    -- Computed once over the merged ProgramData; used to mark such classes
-    -- visually so the user can find the entry-point classes at a glance.
+    -- "Leaf" = no other class has this class as a (direct) superclass.
+    -- These are the entry-point classes: nothing in the program builds
+    -- on top of them. They render at the top of the classes view (the
+    -- dagre layout already places leaves of the subclass-DAG at the top
+    -- with rankDir = TB), with a gold border via the viewer stylesheet.
     referencedAsSuper :: Set.Set Text
     referencedAsSuper = Set.fromList
       [ renderQualName (seSuperclass se)
       | c  <- pdClasses pd
       , se <- ciSuperclasses c
       ]
-    isTopLevel c = not (Set.member (renderQualName (ciName c)) referencedAsSuper)
+    isLeaf c = not (Set.member (renderQualName (ciName c)) referencedAsSuper)
 
     classNodes =
       [ CyNode $ Aeson.object
           [ "id"      Aeson..= renderQualName (ciName c)
-          , "label"   Aeson..= classNodeLabel c
+          , "label"   Aeson..= qnName (ciName c)
           , "kind"    Aeson..= ("class" :: Text)
           , "module"  Aeson..= qnModule (ciName c)
           , "package" Aeson..= qnPackage (ciName c)
           , "tyvars"  Aeson..= ciTyVars c
           , "methods" Aeson..= ciMethods c
-          , "isTopLevel" Aeson..= isTopLevel c
+          , "isLeaf"  Aeson..= isLeaf c
           ]
       | c <- pdClasses pd
       ]
 
-    classNodeLabel c
-      | isTopLevel c = qnName (ciName c) <> "\n\9733 top"  -- ★
-      | otherwise    = qnName (ciName c)
-
     familyNodes =
       [ CyNode $ Aeson.object
           [ "id"      Aeson..= renderQualName (tfName f)