packages feed

scrod-0.2026.2.21: source/library/Scrod/Convert/FromGhc/Merge.hs

-- | Merge items that share the same name.
--
-- When a declaration has both a type signature and a binding (or
-- multiple clauses), the GHC AST produces separate items for each.
-- This module merges them into a single item, combining their
-- documentation and signatures, and remaps child parent keys
-- accordingly.
module Scrod.Convert.FromGhc.Merge where

import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Map as Map
import qualified Data.Maybe as Maybe
import qualified Scrod.Convert.FromGhc.Internal as Internal
import qualified Scrod.Core.Doc as Doc
import qualified Scrod.Core.Item as Item
import qualified Scrod.Core.ItemKey as ItemKey
import qualified Scrod.Core.ItemKind as ItemKind
import qualified Scrod.Core.ItemName as ItemName
import qualified Scrod.Core.Located as Located

-- | Merge items that share the same name.
mergeItemsByName :: [Located.Located Item.Item] -> [Located.Located Item.Item]
mergeItemsByName items =
  let mergeMap = buildMergeMap items
      keyRemapping = buildKeyRemapping items mergeMap
   in Maybe.mapMaybe (applyMerge mergeMap keyRemapping) items

-- | Build a map from item name to the merged item for that name.
buildMergeMap ::
  [Located.Located Item.Item] ->
  Map.Map ItemName.ItemName (Located.Located Item.Item)
buildMergeMap items =
  let namedCandidates =
        Maybe.mapMaybe
          (\item -> fmap (\n -> (n, item NonEmpty.:| [])) . Item.name $ Located.value item)
          (filter isMergeCandidate items)
      groups = Map.fromListWith (<>) namedCandidates
   in Map.map mergeItemGroup groups

-- | Check if an item is eligible for merging.
--
-- Standalone kind signatures are excluded because they are merged
-- into their corresponding declarations in a separate pass (see
-- 'KindSigParents').
isMergeCandidate :: Located.Located Item.Item -> Bool
isMergeCandidate item =
  let val = Located.value item
   in Maybe.isNothing (Item.parentKey val)
        && Maybe.isJust (Item.name val)
        && Item.kind val /= ItemKind.StandaloneKindSig

-- | Merge a group of items sharing the same name into a single item.
mergeItemGroup :: NonEmpty.NonEmpty (Located.Located Item.Item) -> Located.Located Item.Item
mergeItemGroup (single NonEmpty.:| []) = single
mergeItemGroup group =
  let sorted = NonEmpty.sortWith Located.location group
      firstItem = NonEmpty.head sorted
      combinedDoc = foldr (Internal.appendDoc . Item.documentation . Located.value) Doc.Empty sorted
      combinedSince =
        Maybe.listToMaybe . Maybe.mapMaybe (Item.since . Located.value) $ NonEmpty.toList sorted
      combinedSig =
        Maybe.listToMaybe . Maybe.mapMaybe (Item.signature . Located.value) $ NonEmpty.toList sorted
      mergedItem =
        (Located.value firstItem)
          { Item.documentation = combinedDoc,
            Item.since = combinedSince,
            Item.signature = combinedSig
          }
   in firstItem {Located.value = mergedItem}

-- | Build a mapping from old item keys to new merged item keys.
buildKeyRemapping ::
  [Located.Located Item.Item] ->
  Map.Map ItemName.ItemName (Located.Located Item.Item) ->
  Map.Map ItemKey.ItemKey ItemKey.ItemKey
buildKeyRemapping items mergeMap =
  Map.fromList $ concatMap (findRemapping mergeMap) items

-- | Find key remappings for a single item.
findRemapping ::
  Map.Map ItemName.ItemName (Located.Located Item.Item) ->
  Located.Located Item.Item ->
  [(ItemKey.ItemKey, ItemKey.ItemKey)]
findRemapping mergeMap item =
  let val = Located.value item
      itemKey = Item.key val
   in case Item.name val of
        Nothing -> []
        Just name ->
          case Map.lookup name mergeMap of
            Nothing -> []
            Just merged ->
              let mergedKey = Item.key $ Located.value merged
               in if itemKey /= mergedKey && isMergeCandidate item
                    then [(itemKey, mergedKey)]
                    else []

-- | Apply merge: either drop (remapped away) or replace with merged version.
applyMerge ::
  Map.Map ItemName.ItemName (Located.Located Item.Item) ->
  Map.Map ItemKey.ItemKey ItemKey.ItemKey ->
  Located.Located Item.Item ->
  Maybe (Located.Located Item.Item)
applyMerge mergeMap keyRemapping item =
  let val = Located.value item
      itemKey = Item.key val
   in if Map.member itemKey keyRemapping
        then Nothing
        else
          let updatedItem = updateParentKey keyRemapping item
           in case Item.name val of
                Nothing -> Just updatedItem
                Just name ->
                  if not (isMergeCandidate item)
                    then Just updatedItem
                    else case Map.lookup name mergeMap of
                      Nothing -> Just updatedItem
                      Just merged ->
                        if Item.key (Located.value merged) == itemKey
                          then Just merged
                          else Just updatedItem

-- | Update an item's parent key according to the remapping.
updateParentKey ::
  Map.Map ItemKey.ItemKey ItemKey.ItemKey ->
  Located.Located Item.Item ->
  Located.Located Item.Item
updateParentKey keyRemapping locatedItem =
  let val = Located.value locatedItem
   in case Item.parentKey val of
        Nothing -> locatedItem
        Just pk ->
          case Map.lookup pk keyRemapping of
            Nothing -> locatedItem
            Just newPk ->
              locatedItem
                { Located.value =
                    val {Item.parentKey = Just newPk}
                }