tilia-0.0.1.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, startPoint)
import Tilia.Span.Ghc (spanOf, spanOfSrcSpan)
-- | Whether an explicit @import Prelude@ is telling the reader anything.
data PreludeImport
= -- | @ImplicitPrelude@ is on, so the module has the Prelude whatever it
-- says, and the line only trims what it already takes.
Refines
| -- | @ImplicitPrelude@ is off, so the line is the only reason the module
-- has a Prelude at all, and it is an import like any other.
Provides
deriving (Eq, Show)
-- | Sort a module's imports and fold together the ones that say the same
-- thing.
normalizeImports ::
-- | Whether @ImplicitPrelude@ is on
Choice "implicitPrelude" ->
-- | Source lines the block must not be sorted across
[Int] ->
-- | The module's comments
[Comment] ->
-- | Original imports
[LImportDecl GhcPs] ->
-- | Normalized imports
[LImportDecl GhcPs]
normalizeImports implicitPrelude barriers written imports =
concatMap stretch (segmented (dividing imports barriers) tidied)
where
prelude = if isTrue implicitPrelude then Refines else Provides
tidied = map (fmap (tidyList written)) imports
stretch is = foldRuns (fuse written) [((identity prelude 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
-- | Where every name an import lists begins, the names inside a thing's own
-- brackets among them.
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
_ -> []
-- | The lines that fall between imports, out of the lines that must not be
-- sorted across.
dividing :: [LImportDecl GhcPs] -> [Int] -> [Int]
dividing imports = filter (not . within)
where
within l = any (\(from, to) -> from <= l && l <= to) spans'
spans' = [(srcLocLine from, srcLocLine to) | i <- imports, Just (from, to) <- [endsOf i]]
endsOf i = case (srcSpanStart (getLocA i), srcSpanEnd (getLocA i)) of
(RealSrcLoc from _, RealSrcLoc to _) -> Just (from, to)
_ -> Nothing
-- | Cut a list of imports into the stretches the barriers leave between
-- them, in order.
segmented :: [Int] -> [LImportDecl GhcPs] -> [[LImportDecl GhcPs]]
segmented [] imports = [imports]
segmented barriers imports =
groupBy ((==) `on` fst) [(between i, i) | i <- imports] & map (map snd)
where
between i = length (takeWhile (< lineOf i) barriers)
lineOf i = case srcSpanStart (getLocA i) of
RealSrcLoc l _ -> srcLocLine l
_ -> 0
----------------------------------------------------------------------------
-- Runs
-- | Sort by the keys, then replace each run of equal keys by one value
-- folded out of it.
--
-- The sort is stable, so a run holds its values in the order they were
-- written and the fold sees them that way round. That is worth having:
-- folding keeps the first one's identity, and \"first\" should mean first
-- in the file.
foldRuns :: (Ord k) => (a -> a -> a) -> [(k, a)] -> [a]
foldRuns fold' =
map (foldl1 fold' . map snd) . groupBy ((==) `on` fst) . sortOn fst
----------------------------------------------------------------------------
-- Which imports are the same import
-- | What has to agree before two imports may be folded together, in the
-- order imports should be printed in.
--
-- The two leading keys 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.
identity ::
PreludeImport ->
LImportDecl GhcPs ->
(Bool, (Int, Text), Text, Bool, Bool, Bool, Maybe Text, Maybe Bool, Maybe Bool)
identity prelude (L _ decl) =
( prelude == Refines && named (ideclName decl) == T.pack "Prelude",
package (ideclPkgQual decl),
named (ideclName decl),
ideclSource decl == IsBoot,
ideclSafe decl,
isImportDeclQualified (ideclQualified decl),
named <$> ideclAs decl,
hides . fst <$> ideclImportList decl,
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
----------------------------------------------------------------------------
-- Folding two imports into one
-- | Keep the first import and give it everything the second named.
--
-- The result covers both of their spans. That matters for comments: one
-- written between the two has to land inside the declaration that replaces
-- them, and a folded import claiming only the first one's span would leave
-- it nowhere to go.
fuse :: [Comment] -> LImportDecl GhcPs -> LImportDecl GhcPs -> LImportDecl GhcPs
fuse 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 (widened written l l') (tidyItems written (xs <> ys)))
both _ _ = Nothing
----------------------------------------------------------------------------
-- The names inside an import list
tidyList :: [Comment] -> ImportDecl GhcPs -> ImportDecl GhcPs
tidyList written decl =
decl {ideclImportList = fmap (fmap (tidyItems written)) <$> ideclImportList decl}
-- | Sort an import list and fold together the entries naming one thing.
--
-- @import M (T (A), T (B))@ names one type twice and comes out as @import M
-- (T (A, B))@.
tidyItems :: [Comment] -> [LIE GhcPs] -> [LIE GhcPs]
tidyItems written items
-- An import list should hold nothing but names, and the parser will accept
-- things there that the compiler goes on to reject—@import M (module N)@
-- among them. Sorting a list we cannot read would be guessing.
| any (unnameable . unLoc) items = items
| otherwise = foldRuns (wider written) [(nameOf (unLoc i), fmap sortSubnames i) | i <- items]
where
unnameable = \case
IEVar {} -> False
IEThingAbs {} -> False
IEThingAll {} -> False
IEThingWith {} -> False
_ -> True
-- | Cover both of these regions, if anything was written between them.
--
-- A region says two things at once: where a comment written inside it
-- belongs, and how the construct was laid out. What comes out of folding was
-- never written, so it has no layout of its own, and taking the region that
-- covers everything folded in would have it laid out across all the lines
-- those names were spread over—several lines for a name or two.
--
-- So the region grows only where growing it is the point: when a comment
-- falls between the two, and would otherwise be left outside the entry that
-- now holds the names it was written among.
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.
--
-- Naming all of a type beats naming some of its pieces, which beats naming
-- the type alone. Where both name some, the two lists go together. The
-- documentation is dropped whenever two entries are folded: it was written
-- against one of them and would become a claim about both.
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 _ _ wildcard' subs' _) ->
IEThingWith
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
sortSubnames :: IE GhcPs -> IE GhcPs
sortSubnames = \case
IEThingWith x n wildcard subs doc ->
IEThingWith x n wildcard (dedupeSubnames subs) doc
other -> other
-- | The same name written twice in a sub-list is written once here. Which
-- of the two survives cannot matter: they name the same thing.
dedupeSubnames :: [LIEWrappedName GhcPs] -> [LIEWrappedName GhcPs]
dedupeSubnames subs = foldRuns const [(nameKey (unLoc s), s) | s <- subs]
----------------------------------------------------------------------------
-- Ordering names
nameOf :: IE GhcPs -> (Int, Bool, String)
nameOf = \case
IEVar _ x _ -> nameKey (unLoc x)
IEThingAbs _ x _ -> nameKey (unLoc x)
IEThingAll _ x _ -> nameKey (unLoc x)
IEThingWith _ x _ _ _ -> nameKey (unLoc x)
-- 'tidyItems' has already refused to touch a list holding anything else.
_ -> (maxBound, True, "")
-- | Where a name sorts.
--
-- 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 names spelled with
-- letters 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.
nameKey :: IEWrappedName GhcPs -> (Int, Bool, String)
nameKey = \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 -> (Int, Bool, String)
spelled kind (unLoc -> name) = (kind, punctuation text, text)
where
text = occNameString (rdrNameOcc name)
punctuation = \case
(c : _) -> not (isAlphaNum c)
[] -> False