]> Repositorios git - classgraph.git/commitdiff
Extract data-family RHS constructors and show them in the viewer
authorJavier Sagredo <[email protected]>
Thu, 7 May 2026 00:32:36 +0000 (02:32 +0200)
committerJavier Sagredo <[email protected]>
Thu, 7 May 2026 00:32:36 +0000 (02:32 +0200)
Adds a DataConInfo record (dcName + dcArgs + dcFieldLabels) and a
fiDataCons :: [DataConInfo] field on FamInstInfo. Schema/JSON
encoding is backwards-compatible: missing fiDataCons defaults to [].

Extract.hs's extractFamInst now walks the rep TyCon's
tyConDataCons (pulled directly out of the DataFamilyInst flavour
constructor — famInstTyCon turned out to be the family TyCon, not
the rep, on at least 9.14.1) and turns each DataCon into a
DataConInfo. The data constructor's arg types are extracted with
the fam-instance's tyvars in scope, so positional TyVarRefs resolve
to the right fiTyVars entries.

Viewer:
* renderFamInstHead now shows `Family args = Con1 t1 t2 | Con2 …`
  for data fam-instances (record syntax: `Con { f1 :: t1, f2 :: t2 }`).
* The fam-instance side panel grows a Constructors entry listing
  each data constructor with its fields.

Demo: the existing Crate Int / Crate Bool / Crate [a] data
fam-instances now render with their CrateInt / CrateBool / CrateList
constructors visible.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
data/viewer.css
data/viewer.js
src/Classgraph/Extract.hs
src/Classgraph/Schema.hs

index 92cc21a720c55a8d9c4830db18d0c5a79d2facd7..bbb9108a2d8f762a31a924211efb282ea64b87d0 100644 (file)
@@ -142,6 +142,18 @@ body { display: flex; }
 .swatch-pred     { background: #eef2ff; color: #3730a3; border-color: #6366f1; }
 .swatch-faminst  { background: #f5f3ff; color: #5b21b6; border-color: #a78bfa; font-style: italic; }
 .swatch-faminst-unres { background: #f3f4f6; color: #374151; border-color: #9ca3af; font-style: italic; border-style: dashed; }
+ul.datacons {
+  margin: 4px 0 0;
+  padding-left: 0;
+  list-style: none;
+}
+ul.datacons li {
+  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
+  font-size: 12px;
+  line-height: 1.5;
+  padding: 2px 0;
+  color: #1e293b;
+}
 .swatch-external { background: #e5e7eb; color: #374151; border-color: #9ca3af; border-style: dashed; }
 .swatch-ghost    { background: #dbeafe; color: #1e3a8a; border-color: #1d4ed8; border-style: dashed; opacity: 0.85; }
 .swatch-orphan   { background: #ecfdf5; color: #065f46; border: 1px dashed #dc2626; }
index c58b11041f0aab95a8f911bac969507bfb03655e..0f81a654b8dd17f35e605626bbb718c1f89dd946 100644 (file)
   function renderFamInstHead(fi) {
     const args = renderArgsCompact(fi.fiArgs, fi.fiTyVars);
     const head = fi.fiFamily.qnName + (args ? ' ' + args : '');
-    if (fi.fiIsData) return head;
+    if (fi.fiIsData) {
+      // Show the user-declared data constructors on the right-hand
+      // side, using `|` between alternatives. Empty fiDataCons (older
+      // dumps, or extraction edge cases) just degrade to the bare
+      // head with no `=`.
+      const dcs = fi.fiDataCons || [];
+      if (dcs.length === 0) return head;
+      const cons = dcs.map(dc => renderDataCon(dc, fi.fiTyVars)).join(' | ');
+      return head + ' = ' + cons;
+    }
     return head + ' = ' + renderArg(fi.fiRhs, fi.fiTyVars);
   }
 
+  // Render a single data constructor as `Name arg1 arg2` for positional
+  // syntax or `Name { f1 :: arg1, f2 :: arg2 }` for record syntax.
+  function renderDataCon(dc, boundTvs) {
+    const name = dc.dcName || '?';
+    const args = dc.dcArgs || [];
+    const labels = dc.dcFieldLabels || [];
+    if (args.length === 0) return name;
+    if (labels.length === args.length) {
+      // Record syntax: pair each arg with its field label.
+      const fields = args.map((a, i) =>
+        labels[i] + ' :: ' + renderArg(a, boundTvs)).join(', ');
+      return name + ' { ' + fields + ' }';
+    }
+    return name + ' ' + args.map(a => renderArg(a, boundTvs)).join(' ');
+  }
+
   // Render the argument list of a class/family/instance application as
   // it would appear in source: space-separated, with each individual arg
   // self-parenthesising when it's a multi-arg tycon (handled by
       `<dd><ul>${fi.fiTyVars.map(v =>
         `<li>${escape(v.tvName)}<span style="color:#888"> :: ${escape(v.tvKind)}</span></li>`).join('')}</ul></dd>`;
     const src = renderDefinedAt(fi.fiSrc, fi.fiDefinedIn || fi.fiFamily.qnPackage);
-    // Data fam-instances: omit the "Right-hand side" row entirely.
-    // What GHC stores as the rhs is the synthetic R: TyCon, and our
-    // R: rewrite turns it into a copy of the LHS — neither is the
-    // user's actual data-constructor declaration. We don't extract
-    // the data constructors themselves into the schema, so we have
-    // nothing useful to show.
-    const rhsEntry = fi.fiIsData
-      ? ''
-      : `<dt>Right-hand side</dt><dd>${escape(renderArg(fi.fiRhs, fi.fiTyVars))}</dd>`;
+    // Type fam-instances show their RHS directly. Data fam-instances
+    // get a "Constructors" entry listing each user-declared data
+    // constructor with its arg types (or record fields).
+    let rhsEntry;
+    if (fi.fiIsData) {
+      const dcs = fi.fiDataCons || [];
+      if (dcs.length === 0) {
+        rhsEntry = '';
+      } else {
+        const lis = dcs.map(dc =>
+          `<li>${escape(renderDataCon(dc, fi.fiTyVars))}</li>`).join('');
+        rhsEntry = `<dt>Constructors</dt><dd><ul class="datacons">${lis}</ul></dd>`;
+      }
+    } else {
+      rhsEntry = `<dt>Right-hand side</dt><dd>${escape(renderArg(fi.fiRhs, fi.fiTyVars))}</dd>`;
+    }
     return `
       <h2>${head}</h2>
       <p class="pkgmod">${escape(fi.fiFamily.qnPackage)} · ${escape(fi.fiFamily.qnModule)}</p>
index ebd0b8d40d821a408317ee5be376f975d17507ca..660d1611048daf9ff586c6208d75382d2940fde6 100644 (file)
@@ -58,6 +58,7 @@ import GHC.Core.TyCon
   , isFamilyTyCon
   , isInvisibleTyConBinder
   , tyConBinders
+  , tyConDataCons
   , tyConFamInst_maybe
   , tyConName
   , tyConResKind
@@ -65,6 +66,16 @@ import GHC.Core.TyCon
   , tyConTyVars
   , tyConClass_maybe
   )
+import GHC.Core.DataCon
+  ( DataCon
+  , dataConFieldLabels
+  , dataConName
+  , dataConOrigArgTys
+  )
+import GHC.Core.TyCo.Rep (Scaled (..))
+import GHC.Types.FieldLabel (flLabel)
+import Language.Haskell.Syntax.Basic (field_label)
+import GHC.Data.FastString (unpackFS)
 import GHC.Core.Class (classTyCon)
 import GHC.Types.Basic (TupleSort (..))
 import GHC.Core.Type
@@ -361,6 +372,7 @@ extractClosedBranch tc br = FamInstInfo
     -- documentation on the family declaration itself ends up on tfDoc
     -- of the enclosing TypeFamilyInfo.
   , fiDefinedIn = Nothing  -- filled in by Classgraph.Merge
+  , fiDataCons  = []       -- closed branches are always type-family equations
   }
 
 extractFamInst :: Maybe Docs -> FamInst -> FamInstInfo
@@ -390,6 +402,23 @@ extractFamInst mDocs fi = FamInstInfo
     -- separate Haddocks, all of them surface here on every instance —
     -- a known limitation of the GHC docs map's structure.
   , fiDefinedIn = Nothing  -- filled in by Classgraph.Merge
+  , fiDataCons = case fi_flavor fi of
+      SynFamilyInst         -> []
+      DataFamilyInst rep_tc -> map (extractDataCon (fi_tvs fi))
+                                   (tyConDataCons rep_tc)
+  }
+
+-- | Extract a 'DataConInfo' from a 'DataCon'. @boundTvs@ is the
+-- enclosing fam-instance's tyvars (so positional 'TyVarRef' indices
+-- inside the constructor's arg types resolve to the right 'fiTyVars'
+-- entries).
+extractDataCon :: [Var] -> DataCon -> DataConInfo
+extractDataCon boundTvs dc = DataConInfo
+  { dcName        = T.pack (occNameString (nameOccName (dataConName dc)))
+  , dcArgs        = map (\(Scaled _ ty) -> typeArg boundTvs ty)
+                        (dataConOrigArgTys dc)
+  , dcFieldLabels = map (T.pack . unpackFS . field_label . flLabel)
+                        (dataConFieldLabels dc)
   }
 
 ------------------------------------------------------------------------------
index 89dd48858235b270f7c7d8013fbf234600d24a5f..af427bea4ec0f934f0598c004739d1eebfa9a2d6 100644 (file)
@@ -19,6 +19,7 @@ module Classgraph.Schema
   , TypeFamilyFlavor (..)
   , TypeFamilyInfo (..)
   , FamInstInfo (..)
+  , DataConInfo (..)
   , SrcSpanInfo (..)
   ) where
 
@@ -273,6 +274,11 @@ data FamInstInfo = FamInstInfo
   , fiDefinedIn :: !(Maybe Text)
     -- ^ Defining-package id (normalised), set at merge time. Used the
     -- same way as 'iiDefinedIn' for editor-link source-root lookup.
+  , fiDataCons :: ![DataConInfo]
+    -- ^ For data fam-instances: the user-declared data constructors
+    -- on the right-hand side of @data instance F … = …@. Always
+    -- empty for type fam-instances (where the RHS is a type, not a
+    -- list of constructors).
   }
   deriving stock (Generic, Show, Eq)
 
@@ -286,6 +292,7 @@ instance ToJSON FamInstInfo where
     , "fiIsData"    .= fiIsData fi
     , "fiDoc"       .= fiDoc fi
     , "fiDefinedIn" .= fiDefinedIn fi
+    , "fiDataCons"  .= fiDataCons fi
     ]
 
 instance FromJSON FamInstInfo where
@@ -298,6 +305,41 @@ instance FromJSON FamInstInfo where
     <*> o .:? "fiIsData" .!= False
     <*> o .:? "fiDoc"
     <*> o .:? "fiDefinedIn"
+    <*> o .:? "fiDataCons" .!= []
+
+-- | Data constructor of a @data instance@ declaration.
+--
+-- For @data instance Crate Int = CrateInt Int Bool@:
+--   * @dcName        = "CrateInt"@
+--   * @dcArgs        = [TyConApp Int [], TyConApp Bool []]@ (positional)
+--   * @dcFieldLabels = []@ (no record syntax)
+--
+-- For @data instance Foo Int = Foo { x :: Int, y :: Bool }@:
+--   * @dcName        = "Foo"@
+--   * @dcArgs        = [TyConApp Int [], TyConApp Bool []]@
+--   * @dcFieldLabels = ["x", "y"]@ (parallel to dcArgs)
+--
+-- @TyVarRef i@ inside @dcArgs@ refers to the enclosing 'fiTyVars' of
+-- the 'FamInstInfo'.
+data DataConInfo = DataConInfo
+  { dcName        :: !Text
+  , dcArgs        :: ![TypeArg]
+  , dcFieldLabels :: ![Text]
+  }
+  deriving stock (Generic, Show, Eq)
+
+instance ToJSON DataConInfo where
+  toJSON d = object
+    [ "dcName"        .= dcName d
+    , "dcArgs"        .= dcArgs d
+    , "dcFieldLabels" .= dcFieldLabels d
+    ]
+
+instance FromJSON DataConInfo where
+  parseJSON = withObject "DataConInfo" $ \o -> DataConInfo
+    <$> o .:  "dcName"
+    <*> o .:  "dcArgs"
+    <*> o .:? "dcFieldLabels" .!= []
 
 data SrcSpanInfo = SrcSpanInfo
   { ssFile      :: !Text