packages feed

tilia-0.1.0.0: src/Tilia/Imports.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns #-}

-- | Putting a module's imports in order.
module Tilia.Imports
  ( normalizeImports,
  )
where

import Data.Char (isAlphaNum)
import Data.Choice (Choice, isTrue)
import Data.Function (on, (&))
import Data.List (groupBy, sortOn)
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Data.FastString (unpackFS)
import GHC.Hs
import GHC.Types.Name.Occurrence (occNameString)
import GHC.Types.Name.Reader (RdrName, rdrNameOcc)
import GHC.Types.PkgQual (RawPkgQual (..))
import GHC.Types.SourceText (StringLiteral (..))
import GHC.Types.SrcLoc
import Tilia.Comments (Comment (..), commentTrailing, commentsWithin)
import Tilia.Span (endPoint, isSingleLine, startPoint)
import Tilia.Span.Ghc (bracketsSpan, spanOf, spanOfSrcSpan, tokenSpan)

-- | Sort and fold together a module's imports.
normalizeImports ::
  -- | Whether @ImplicitPrelude@ is on.
  Choice "implicitPrelude" ->
  -- | Source lines the block must not be sorted across.
  [Int] ->
  -- | Source lines the names of a list must not be sorted across.
  [Int] ->
  -- | The module's comments.
  [Comment] ->
  -- | Original imports.
  [LImportDecl GhcPs] ->
  -- | Normalized imports.
  [LImportDecl GhcPs]
normalizeImports implicitPrelude barriers directives written imports =
  concatMap stretch (cutAtBarriers barriers tidied)
  where
    tidied = fmap (fmap (tidyImportList directives written)) imports
    stretch is =
      foldRuns
        (combineImports directives written)
        [((importIdentity implicitPrelude i, hiddenNames i, alone i), i) | i <- is]
    alone i
      | any strands (spanOf i) = startLineOf i
      | otherwise = 0
      where
        strands s =
          any
            (unanchored (itemStarts i))
            (filter loose (commentsWithin s written))
        loose = not . commentTrailing
    unanchored starts c = not (any (> endPoint (commentSpan c)) starts)
    startLineOf i = case srcSpanStart (getLocA i) of
      RealSrcLoc l _ -> srcLocLine l
      _ -> 0

-- | The names an import hides, where it hides any, so that only imports
-- hiding the same names are folded together.
hiddenNames ::
  LImportDecl GhcPs ->
  Maybe [(NameIdentity, Maybe (Bool, [NameIdentity]))]
hiddenNames (L _ decl) = case ideclImportList decl of
  Just (EverythingBut, L _ items) -> Just (fmap (hidden . unLoc) items)
  _ -> Nothing
  where
    hidden item = (nameIdentity item, subordinates item)
    subordinates = \case
      IEThingAll{} -> Just (True, [])
      IEThingWith _ _ NoIEWildcard subs _ -> Just (False, names subs)
      IEThingWith _ _ (IEWildcard _) subs _ -> Just (True, names subs)
      _ -> Nothing
    names = fmap (wrappedNameIdentity . unLoc)

-- | Where every name an import lists begins, each as a line and a column.
--
-- The names inside a thing's own brackets count too, so @T (A, B)@ gives
-- three positions: the start of @T@, of @A@ and of @B@. An import with no
-- list has none at all.
itemStarts :: LImportDecl GhcPs -> [(Int, Int)]
itemStarts (L _ decl) = case ideclImportList decl of
  Nothing -> []
  Just (_, L _ items) -> concatMap starts items
  where
    starts item =
      foldMap ((: []) . startPoint) (spanOf item)
        <> inside (unLoc item)
    inside = \case
      IEThingWith _ _ _ members _ ->
        concatMap (foldMap ((: []) . startPoint) . spanOf) members
      _ -> []

-- | Cut imports, or the names of a list, into the stretches the barriers
-- leave between them.
cutAtBarriers ::
  -- | Lines not to be sorted across, in ascending order.
  [Int] ->
  -- | What to cut, as it was written.
  [GenLocated SrcSpanAnnA a] ->
  -- | One stretch per run between barriers, in the same order.
  [[GenLocated SrcSpanAnnA a]]
cutAtBarriers [] xs = [xs]
cutAtBarriers barriers xs =
  groupBy ((==) `on` fst) [(between i, i) | i <- xs] & fmap (fmap snd)
  where
    between i = length (takeWhile (< lineOf i) barriers)
    lineOf i = case srcSpanStart (getLocA i) of
      RealSrcLoc l _ -> srcLocLine l
      _ -> 0

-- | Sort by the keys, then replace each run of equal keys by one value
-- folded out of it.
foldRuns ::
  (Ord k) =>
  -- | How to fold two values that share a key.
  (a -> a -> a) ->
  -- | The values, each under the key it sorts and folds by.
  [(k, a)] ->
  -- | One value per distinct key, in key order.
  [a]
foldRuns fold' =
  fmap (foldl1 fold' . fmap snd) . groupBy ((==) `on` fst) . sortOn fst

-- | What has to agree before two imports may be folded together.
--
-- The derived ordering is the whole of the policy: the fields are compared
-- in the order they are declared, which is the order imports are printed
-- in. The two leading fields are about reading rather than about identity.
-- A @Prelude@ that only refines what the module already has goes at the
-- end, since looking for it among the @D@s would be looking for the least
-- interesting line in the block. The package goes before the module name so
-- that the imports from one package stay in one run; sorting by module
-- first would interleave them and hide who provides what.
data ImportIdentity = ImportIdentity
  { -- | A @Prelude@ that only refines what the module already has.
    iiRefinesPrelude :: Bool,
    -- | The package it names, where it names one.
    iiPackage :: (Int, Text),
    -- | The module it brings in.
    iiModule :: Text,
    -- | Written @{-\# SOURCE \#-}@?
    iiBoot :: Bool,
    -- | Written @safe@?
    iiSafe :: Bool,
    -- | Written @qualified@?
    iiQualified :: Bool,
    -- | The name it was given with @as@, where it was given one.
    iiAlias :: Maybe Text,
    -- | Whether its list hides rather than names, where it has a list.
    iiHides :: Maybe Bool,
    -- | Whether it is a @quote@ rather than a @splice@ import, where it is
    -- levelled at all.
    iiLevel :: Maybe Bool
  }
  deriving (Eq, Ord, Show)

-- | The identity of one import.
importIdentity ::
  -- | Whether @ImplicitPrelude@ is on.
  Choice "implicitPrelude" ->
  -- | The import.
  LImportDecl GhcPs ->
  ImportIdentity
importIdentity implicitPrelude (L _ decl) =
  ImportIdentity
    { iiRefinesPrelude =
        isTrue implicitPrelude && named (ideclName decl) == T.pack "Prelude",
      iiPackage = package (ideclPkgQual decl),
      iiModule = named (ideclName decl),
      iiBoot = ideclSource decl == IsBoot,
      iiSafe = ideclSafe decl,
      iiQualified = isImportDeclQualified (ideclQualified decl),
      iiAlias = named <$> ideclAs decl,
      iiHides = hides . fst <$> ideclImportList decl,
      iiLevel = lifted (ideclLevelSpec decl)
    }
  where
    named = T.pack . moduleNameString . unLoc
    package = \case
      NoRawPkgQual -> (0, T.empty)
      RawPkgQual (sl_fs -> fs)
        | name == T.pack "this" -> (2, T.empty)
        | otherwise -> (1, name)
        where
          name = T.pack (unpackFS fs)
    hides = \case
      Exactly -> False
      EverythingBut -> True
    lifted = \case
      NotLevelled -> Nothing
      LevelStylePre l -> Just (quoted l)
      LevelStylePost l -> Just (quoted l)
    quoted = \case
      ImportDeclSplice -> False
      ImportDeclQuote -> True

-- | Keep the first import and give it everything the second named.
combineImports ::
  -- | Source lines the names of a list must not be sorted across.
  [Int] ->
  -- | The module's comments, which decide how far the span may grow.
  [Comment] ->
  -- | The import kept, whose spelling the result takes.
  LImportDecl GhcPs ->
  -- | The import folded in, which gives up its list and its span.
  LImportDecl GhcPs ->
  -- | The first, listing what both named, across both their spans.
  LImportDecl GhcPs
combineImports directives written (L ann kept) (L other folded) =
  L
    ann{entry = EpaSpan (combineSrcSpans (locA ann) (locA other))}
    kept{ideclImportList = both (ideclImportList kept) (ideclImportList folded)}
  where
    both (Just (interpretation, L l xs)) (Just (_, L l' ys)) =
      Just
        ( interpretation,
          L
            (bracketsAcross (widened written l l') l')
            (tidyImportItems directives written interpretation (xs <> ys))
        )
    both _ _ = Nothing

-- | Take the second list's brackets where only they were written across
-- lines, so that a list folded out of several breaks if any of them did.
bracketsAcross :: EpAnn (AnnList a) -> EpAnn (AnnList a) -> EpAnn (AnnList a)
bracketsAcross kept folded
  | onOneLine kept && not (onOneLine folded) =
      kept{anns = (anns kept){al_brackets = al_brackets (anns folded)}}
  | otherwise = kept
  where
    onOneLine = all isSingleLine . bracketsSpan . al_brackets . anns

-- | An import with its list sorted and the entries naming one thing folded
-- together. An import with no list is left as it is.
tidyImportList :: [Int] -> [Comment] -> ImportDecl GhcPs -> ImportDecl GhcPs
tidyImportList directives written decl =
  decl
    { ideclImportList = tidied <$> ideclImportList decl
    }
  where
    tidied (interpretation, items) =
      ( interpretation,
        tidyImportItems directives written interpretation <$> items
      )

-- | Sort an import list and fold together the entries naming one thing,
-- each stretch between directives on its own.
--
-- @import M (T (A), T (B))@ names one type twice and comes out as @import M
-- (T (A, B))@.
tidyImportItems ::
  [Int] ->
  [Comment] ->
  ImportListInterpretation ->
  [LIE GhcPs] ->
  [LIE GhcPs]
tidyImportItems directives written interpretation items
  | any (unnameable . unLoc) items = items
  | otherwise = concatMap tidied (cutAtBarriers directives items)
  where
    tidied stretch =
      foldRuns
        (wider written)
        [ ((nameIdentity (unLoc i), apart (unLoc i)), fmap sortSubnames i)
        | i <- stretch
        ]
    -- A bare name in a hiding list also hides any data constructor of that
    -- name, which the name with its own parentheses does not.
    apart item = case (interpretation, item) of
      (EverythingBut, IEThingAll{}) -> True
      (EverythingBut, IEThingWith{}) -> True
      _ -> False
    unnameable = \case
      IEVar{} -> False
      IEThingAbs{} -> False
      IEThingAll{} -> False
      IEThingWith{} -> False
      _ -> True

-- | Cover both of these regions, if anything was written between them.
widened :: [Comment] -> EpAnn ann -> EpAnn ann -> EpAnn ann
widened written a b
  | any holdsComment (spanOfSrcSpan combined) = a{entry = EpaSpan combined}
  | otherwise = a
  where
    combined = combineSrcSpans (locA a) (locA b)
    holdsComment s = not (null (commentsWithin s written))

-- | Of two entries naming one thing, the one that brings in more of it.
wider :: [Comment] -> LIE GhcPs -> LIE GhcPs -> LIE GhcPs
wider written (L ann kept) (L other folded) =
  L (widened written ann other) (combine kept folded)
  where
    combine a b = case (a, b) of
      (IEThingAll x n _, _) -> IEThingAll x n Nothing
      (_, IEThingAll x n _) -> IEThingAll x n Nothing
      (IEThingWith x n wildcard subs _, IEThingWith x' _ wildcard' subs' _) ->
        IEThingWith
          (parensAcross x x')
          n
          (eitherWildcard wildcard wildcard')
          (dedupeSubnames (subs <> subs'))
          Nothing
      (IEThingWith x n wildcard subs _, _) ->
        IEThingWith x n wildcard subs Nothing
      (_, IEThingWith x n wildcard subs _) ->
        IEThingWith x n wildcard subs Nothing
      (IEVar _ n _, _) -> IEVar Nothing n Nothing
      _ -> a

    eitherWildcard a b = case (a, b) of
      (NoIEWildcard, NoIEWildcard) -> NoIEWildcard
      _ -> IEWildcard 0

    parensAcross (warning, (open, dots, comma, close)) (_, (open', _, _, close'))
      | onOneLine open close && not (onOneLine open' close') =
          (warning, (open', dots, comma, close'))
      | otherwise = (warning, (open, dots, comma, close))
    onOneLine open close = all isSingleLine (tokenSpan open <> tokenSpan close)

-- | An entry with the names in its own brackets sorted and deduplicated.
sortSubnames :: IE GhcPs -> IE GhcPs
sortSubnames = \case
  IEThingWith x n wildcard subs doc ->
    IEThingWith x n wildcard (dedupeSubnames subs) doc
  other -> other

-- | Deduplicate names in sub-lists.
dedupeSubnames :: [LIEWrappedName GhcPs] -> [LIEWrappedName GhcPs]
dedupeSubnames subs = foldRuns const [(wrappedNameIdentity (unLoc s), s) | s <- subs]

-- | Where a name sorts, and what makes two of them name one thing.
--
-- The derived ordering is the whole of the policy: the fields are compared
-- in the order they are declared. Names are grouped first by what kind of
-- thing is named, so that the @pattern@s and the @type@s of an import list
-- stay together. Then the ones spelled with letters come before the ones
-- spelled with punctuation, which gathers the operators at the end where
-- they are easy to find; ordering by character code would scatter them,
-- some before the letters and some after.
data NameIdentity = NameIdentity
  { -- | Which kind of thing is named: a plain name, then a @default@, a
    -- @pattern@, a @type@ and a @data@.
    niKind :: Int,
    -- | Is it spelled with punctuation rather than letters?
    niPunctuation :: Bool,
    -- | The name as it was written.
    niName :: String
  }
  deriving (Eq, Ord, Show)

-- | The identity of the name an entry is about.
nameIdentity :: IE GhcPs -> NameIdentity
nameIdentity = \case
  IEVar _ x _ -> wrappedNameIdentity (unLoc x)
  IEThingAbs _ x _ -> wrappedNameIdentity (unLoc x)
  IEThingAll _ x _ -> wrappedNameIdentity (unLoc x)
  IEThingWith _ x _ _ _ -> wrappedNameIdentity (unLoc x)
  _ ->
    NameIdentity
      { niKind = maxBound,
        niPunctuation = True,
        niName = ""
      }

-- | The identity of one name, as an import list writes it.
wrappedNameIdentity :: IEWrappedName GhcPs -> NameIdentity
wrappedNameIdentity = \case
  IEName _ x -> spelled 0 x
  IEDefault _ x -> spelled 1 x
  IEPattern _ x -> spelled 2 x
  IEType _ x -> spelled 3 x
  IEData _ x -> spelled 4 x
  where
    spelled :: Int -> LocatedN RdrName -> NameIdentity
    spelled kind (unLoc -> name) =
      NameIdentity
        { niKind = kind,
          niPunctuation = punctuation text,
          niName = text
        }
      where
        text = occNameString (rdrNameOcc name)
    punctuation = \case
      (c : _) -> not (isAlphaNum c)
      [] -> False