hindent-6.1.0: src/HIndent/ModulePreprocessing/CommentRelocation.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs, CPP #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
-- | Comment relocation for pretty-printing comments correctly.
--
-- HIndent gathers all comments above a function, an import, a module
-- declaration, etc. For example, HIndent formats the following code
--
-- > f :: Int
-- > f = 1
-- >
-- > -- A comment between f and g
-- >
-- > -- Another comment between f and g
-- >
-- > g :: Int
-- > g = 2
--
-- to
--
-- > f :: Int
-- > f = 1
-- >
-- > -- A comment between f and g
-- > -- Another comment between f and g
-- > g :: Int
-- > g = 2
--
-- AST nodes must have the information of which comments are above, on the
-- same line, and below. However, AST nodes generated by a parser of
-- 'ghc-lib-parser' only contain comments after them. 'relocateComments' is
-- defined to solve the problem.
module HIndent.ModulePreprocessing.CommentRelocation
( relocateComments
) where
import Control.Exception
import Control.Monad.State
import Data.Foldable
import Data.Function
import Data.List
import GHC.Data.Bag
import GHC.Types.SrcLoc
import Generics.SYB hiding (GT, typeOf, typeRep)
import HIndent.GhcLibParserWrapper.GHC.Hs
import HIndent.Pretty.Pragma
import HIndent.Pretty.SigBindFamily
import Type.Reflection
#if MIN_VERSION_GLASGOW_HASKELL(9,6,0,0)
import Control.Monad
#endif
-- | A wrapper type used in everywhereMEpAnnsBackwards' to collect all
-- 'EpAnn's to apply a function with them in order their positions.
data Wrapper =
forall a. Typeable (EpAnn a) =>
Wrapper (EpAnn a)
-- | 'State' with comments.
type WithComments = State [LEpaComment]
-- | This function collects all comments from the passed 'HsModule', and
-- modifies all 'EpAnn's so that all 'EpAnn's have 'EpaCommentsBalanced's.
relocateComments :: HsModule' -> [LEpaComment] -> HsModule'
relocateComments = evalState . relocate
where
relocate =
relocatePragmas
>=> relocateCommentsBeforePragmas
>=> relocateCommentsInExportList
>=> relocateCommentsInClass
>=> relocateCommentsBeforeTopLevelDecls
>=> relocateCommentsSameLine
>=> relocateCommentsInDoExpr
>=> relocateCommentsInCase
>=> relocateCommentsTopLevelWhereClause
>=> relocateCommentsAfter
>=> assertAllCommentsAreConsumed
assertAllCommentsAreConsumed x = do
cs <- get
assert (null cs) (pure x)
-- | This function locates pragmas to the module's EPA.
#if MIN_VERSION_ghc_lib_parser(9,6,1)
relocatePragmas :: HsModule GhcPs -> WithComments (HsModule GhcPs)
relocatePragmas m@HsModule {hsmodExt = xmod@XModulePs {hsmodAnn = epa@EpAnn {}}} = do
newAnn <- insertComments (isPragma . ac_tok . unLoc) insertPriorComments epa
return m {hsmodExt = xmod {hsmodAnn = newAnn}}
#else
relocatePragmas :: HsModule -> WithComments HsModule
relocatePragmas m@HsModule {hsmodAnn = epa@EpAnn {}} = do
newAnn <- insertComments (isPragma . ac_tok . unLoc) insertPriorComments epa
return m {hsmodAnn = newAnn}
#endif
relocatePragmas m = pure m
-- | This function locates comments that are located before pragmas to the
-- module's EPA.
#if MIN_VERSION_ghc_lib_parser(9,6,1)
relocateCommentsBeforePragmas :: HsModule GhcPs -> WithComments (HsModule GhcPs)
relocateCommentsBeforePragmas m@HsModule {hsmodExt = xmod@XModulePs {hsmodAnn = ann}}
| pragmaExists m = do
newAnn <- insertCommentsByPos (< startPosOfPragmas) insertPriorComments ann
pure m {hsmodExt = xmod {hsmodAnn = newAnn}}
| otherwise = pure m
where
startPosOfPragmas = anchor $ getLoc $ head $ priorComments $ comments ann
#else
relocateCommentsBeforePragmas :: HsModule -> WithComments HsModule
relocateCommentsBeforePragmas m@HsModule {hsmodAnn = ann}
| pragmaExists m = do
newAnn <- insertCommentsByPos (< startPosOfPragmas) insertPriorComments ann
pure m {hsmodAnn = newAnn}
| otherwise = pure m
where
startPosOfPragmas = anchor $ getLoc $ head $ priorComments $ comments ann
#endif
-- | This function locates comments that are located before each element of
-- an export list.
relocateCommentsInExportList :: HsModule' -> WithComments HsModule'
relocateCommentsInExportList =
relocateCommentsBeforeEachElement
elemGetter
elemSetter
annGetter
annSetter
cond
where
elemGetter :: HsModule' -> [LIE GhcPs]
elemGetter HsModule {hsmodExports = Just (L _ xs)} = xs
elemGetter _ = []
elemSetter xs HsModule {hsmodExports = Just (L sp _), ..} =
HsModule {hsmodExports = Just (L sp xs), ..}
elemSetter _ x = x
annGetter (L SrcSpanAnn {..} _) = ann
annSetter newAnn (L SrcSpanAnn {..} x) = L SrcSpanAnn {ann = newAnn, ..} x
cond HsModule {hsmodExports = Just (L SrcSpanAnn {ann = EpAnn {entry = Anchor {anchor = listAnc}}} _)} (L SrcSpanAnn {ann = EpAnn {entry = Anchor {anchor = elemAnc}}} _) comAnc =
srcSpanStartLine comAnc < srcSpanStartLine elemAnc
&& realSrcSpanStart listAnc < realSrcSpanStart comAnc
cond _ _ _ = False
-- | Locates comments before each case branch.
relocateCommentsInCase :: HsModule' -> WithComments HsModule'
relocateCommentsInCase =
relocateCommentsBeforeEachElement
elemGetter
elemSetter
annGetter
annSetter
cond
where
elemGetter :: LHsExpr GhcPs -> [LMatch GhcPs (LHsExpr GhcPs)]
elemGetter (L _ (HsCase _ _ (MG {mg_alts = L _ xs}))) = xs
elemGetter _ = []
elemSetter xs (L sp (HsCase ext expr (MG {mg_alts = L sp' _, ..}))) =
L sp (HsCase ext expr (MG {mg_alts = L sp' xs, ..}))
elemSetter _ x = x
annGetter (L SrcSpanAnn {..} _) = ann
annSetter newAnn (L SrcSpanAnn {..} x) = L SrcSpanAnn {ann = newAnn, ..} x
cond (L SrcSpanAnn {ann = EpAnn {entry = Anchor {anchor = caseAnchor}}} _) (L SrcSpanAnn {ann = EpAnn {entry = Anchor {anchor = branchAnchor}}} _) comAnc =
srcSpanStartLine comAnc < srcSpanStartLine branchAnchor
&& realSrcSpanStart caseAnchor < realSrcSpanStart comAnc
cond _ _ _ = False
-- | Locates comments before each class element.
relocateCommentsInClass :: HsModule' -> WithComments HsModule'
relocateCommentsInClass =
relocateCommentsBeforeEachElement
elemGetter
elemSetter
annGetter
annSetter
cond
where
elemGetter :: LHsDecl GhcPs -> [LSigBindFamily]
elemGetter (L _ (TyClD _ ClassDecl {..})) =
mkSortedLSigBindFamilyList
tcdSigs
(bagToList tcdMeths)
tcdATs
tcdATDefs
[]
elemGetter _ = []
elemSetter xs (L sp (TyClD ext ClassDecl {..})) = L sp (TyClD ext newDecl)
where
newDecl =
ClassDecl
{ tcdSigs = sigs
, tcdMeths = listToBag binds
, tcdATs = typeFamilies
, tcdATDefs = tyFamInsts
, ..
}
(sigs, binds, typeFamilies, tyFamInsts, _) =
destructLSigBindFamilyList xs
elemSetter _ x = x
annGetter (L SrcSpanAnn {..} _) = ann
annSetter newAnn (L SrcSpanAnn {..} x) = L SrcSpanAnn {ann = newAnn, ..} x
cond (L SrcSpanAnn {ann = EpAnn {entry = Anchor {anchor = classAnchor}}} _) (L SrcSpanAnn {ann = EpAnn {entry = Anchor {anchor = elemAnchor}}} _) comAnc =
srcSpanStartLine comAnc < srcSpanStartLine elemAnchor
&& realSrcSpanStart classAnchor < realSrcSpanStart comAnc
cond _ _ _ = False
-- | Locates comments before each statement in a do expression.
relocateCommentsInDoExpr :: HsModule' -> WithComments HsModule'
relocateCommentsInDoExpr =
relocateCommentsBeforeEachElement
elemGetter
elemSetter
annGetter
annSetter
cond
where
elemGetter :: LHsExpr GhcPs -> [ExprLStmt GhcPs]
elemGetter (L _ (HsDo _ DoExpr {} (L _ xs))) = xs
elemGetter (L _ (HsDo _ MDoExpr {} (L _ xs))) = xs
elemGetter _ = []
elemSetter xs (L sp (HsDo ext flavor@DoExpr {} (L sp' _))) =
L sp (HsDo ext flavor (L sp' xs))
elemSetter xs (L sp (HsDo ext flavor@MDoExpr {} (L sp' _))) =
L sp (HsDo ext flavor (L sp' xs))
elemSetter _ x = x
annGetter (L SrcSpanAnn {..} _) = ann
annSetter newAnn (L SrcSpanAnn {..} x) = L SrcSpanAnn {ann = newAnn, ..} x
cond (L SrcSpanAnn {ann = EpAnn {entry = Anchor {anchor = doAnchor}}} _) (L SrcSpanAnn {ann = EpAnn {entry = Anchor {anchor = elemAnchor}}} _) comAnc =
srcSpanStartLine comAnc < srcSpanStartLine elemAnchor
&& realSrcSpanStart doAnchor < realSrcSpanStart comAnc
cond _ _ _ = False
-- | This function locates comments located before top-level declarations.
relocateCommentsBeforeTopLevelDecls :: HsModule' -> WithComments HsModule'
relocateCommentsBeforeTopLevelDecls = everywhereM (applyM f)
where
f epa@EpAnn {..} =
insertCommentsByPos (isBefore $ anchor entry) insertPriorComments epa
f EpAnnNotUsed = pure EpAnnNotUsed
isBefore anc comAnc =
srcSpanStartCol anc == 1
&& srcSpanStartCol comAnc == 1
&& srcSpanStartLine comAnc < srcSpanStartLine anc
-- | This function scans the given AST from bottom to top and locates
-- comments that are on the same line as the node. Comments are stored in
-- the 'followingComments' of 'EpaCommentsBalanced'.
relocateCommentsSameLine :: HsModule' -> WithComments HsModule'
relocateCommentsSameLine = everywhereMEpAnnsBackwards f
where
f epa@EpAnn {..} =
insertCommentsByPos
(isOnSameLine $ anchor entry)
insertFollowingComments
epa
f EpAnnNotUsed = pure EpAnnNotUsed
isOnSameLine anc comAnc =
srcSpanStartLine comAnc == srcSpanStartLine anc
&& srcSpanStartLine comAnc == srcSpanEndLine anc
-- | This function locates comments above the top-level declarations in
-- a 'where' clause in the topmost declaration.
relocateCommentsTopLevelWhereClause :: HsModule' -> WithComments HsModule'
relocateCommentsTopLevelWhereClause m@HsModule {..} = do
hsmodDecls' <- mapM relocateCommentsDeclWhereClause hsmodDecls
pure m {hsmodDecls = hsmodDecls'}
where
relocateCommentsDeclWhereClause (L l (ValD ext fb@(FunBind {fun_matches = MG {..}}))) = do
mg_alts' <- mapM (mapM relocateCommentsMatch) mg_alts
pure $ L l (ValD ext fb {fun_matches = MG {mg_alts = mg_alts', ..}})
relocateCommentsDeclWhereClause x = pure x
relocateCommentsMatch (L l match@Match {m_grhss = gs@GRHSs {grhssLocalBinds = (HsValBinds ext (ValBinds ext' binds sigs))}}) = do
(binds', sigs') <- relocateCommentsBindsSigs binds sigs
let localBinds = HsValBinds ext (ValBinds ext' binds' sigs')
pure $ L l match {m_grhss = gs {grhssLocalBinds = localBinds}}
relocateCommentsMatch x = pure x
relocateCommentsBindsSigs ::
LHsBindsLR GhcPs GhcPs
-> [LSig GhcPs]
-> WithComments (LHsBindsLR GhcPs GhcPs, [LSig GhcPs])
relocateCommentsBindsSigs binds sigs = do
bindsSigs' <- mapM addCommentsBeforeEpAnn bindsSigs
pure (listToBag $ filterLBind bindsSigs', filterLSig bindsSigs')
where
bindsSigs = mkSortedLSigBindFamilyList sigs (bagToList binds) [] [] []
addCommentsBeforeEpAnn (L (SrcSpanAnn epa@EpAnn {..} sp) x) = do
cs <- get
let (notAbove, above) =
partitionAboveNotAbove (sortCommentsByLocation cs) entry
epa' = epa {comments = insertPriorComments comments above}
put notAbove
pure $ L (SrcSpanAnn epa' sp) x
addCommentsBeforeEpAnn x = pure x
partitionAboveNotAbove cs sp =
fst
$ foldr'
(\c@(L l _) ((ls, rs), lastSpan) ->
if anchor l `isAbove` anchor lastSpan
then ((ls, c : rs), l)
else ((c : ls, rs), lastSpan))
(([], []), sp)
cs
isAbove comAnc anc =
srcSpanStartCol comAnc == srcSpanStartCol anc
&& srcSpanEndLine comAnc + 1 == srcSpanStartLine anc
-- | This function scans the given AST from bottom to top and locates
-- comments in the comment pool after each node on it.
relocateCommentsAfter :: HsModule' -> WithComments HsModule'
relocateCommentsAfter = everywhereMEpAnnsBackwards f
where
f epa@EpAnn {..} =
insertCommentsByPos (isAfter $ anchor entry) insertFollowingComments epa
f EpAnnNotUsed = pure EpAnnNotUsed
isAfter anc comAnc = srcSpanEndLine anc <= srcSpanStartLine comAnc
-- | Locates comments before each element in a parent.
relocateCommentsBeforeEachElement ::
forall a b c. Typeable a
=> (a -> [b]) -- ^ Element getter
-> ([b] -> a -> a) -- ^ Element setter
-> (b -> EpAnn c) -- ^ Annotation getter
-> (EpAnn c -> b -> b) -- ^ Annotation setter
-> (a -> b -> RealSrcSpan -> Bool) -- ^ The function to decide whether to locate comments
-> HsModule'
-> WithComments HsModule'
relocateCommentsBeforeEachElement elemGetter elemSetter annGetter annSetter cond =
everywhereM (mkM f)
where
f :: a -> WithComments a
f x = do
newElems <- mapM insertCommentsBeforeElement (elemGetter x)
pure $ elemSetter newElems x
where
insertCommentsBeforeElement element
| elemAnn@EpAnn {} <- annGetter element = do
newEpa <-
insertCommentsByPos (cond x element) insertPriorComments elemAnn
pure $ annSetter newEpa element
| otherwise = pure element
-- | This function applies the given function to all 'EpAnn's.
applyM ::
forall a. Typeable a
=> (forall b. EpAnn b -> WithComments (EpAnn b))
-> (a -> WithComments a)
applyM f
| App g _ <- typeRep @a
, Just HRefl <- eqTypeRep g (typeRep @EpAnn) = f
| otherwise = pure
-- | This function drains comments whose positions satisfy the given
-- predicate and inserts them to the given node using the given inserter.
insertCommentsByPos ::
(RealSrcSpan -> Bool)
-> (EpAnnComments -> [LEpaComment] -> EpAnnComments)
-> EpAnn a
-> WithComments (EpAnn a)
insertCommentsByPos cond = insertComments (cond . anchor . getLoc)
-- | This function drains comments that satisfy the given predicate and
-- inserts them to the given node using the given inserter.
insertComments ::
(LEpaComment -> Bool)
-> (EpAnnComments -> [LEpaComment] -> EpAnnComments)
-> EpAnn a
-> WithComments (EpAnn a)
insertComments cond inserter epa@EpAnn {..} = do
coms <- drainComments cond
pure $ epa {comments = inserter comments coms}
insertComments _ _ EpAnnNotUsed = pure EpAnnNotUsed
-- | This function inserts comments to `priorComments`.
insertPriorComments :: EpAnnComments -> [LEpaComment] -> EpAnnComments
insertPriorComments (EpaComments prior) cs =
EpaComments (sortCommentsByLocation $ prior ++ cs)
insertPriorComments (EpaCommentsBalanced prior following) cs =
EpaCommentsBalanced (sortCommentsByLocation $ prior ++ cs) following
-- | This function inserts comments to `followingComments`.
insertFollowingComments :: EpAnnComments -> [LEpaComment] -> EpAnnComments
insertFollowingComments (EpaComments prior) cs = EpaCommentsBalanced prior cs
insertFollowingComments (EpaCommentsBalanced prior following) cs =
EpaCommentsBalanced prior (sortCommentsByLocation $ following ++ cs)
-- | This function drains comments that satisfy the given predicate.
drainComments :: (LEpaComment -> Bool) -> WithComments [LEpaComment]
drainComments cond = do
coms <- get
let (xs, others) = partition cond coms
put others
return xs
-- | 'everywhereM' but applies the given function to EPAs in order their
-- positions from backwards.
everywhereMEpAnnsBackwards ::
Data a
=> (forall b. EpAnn b -> WithComments (EpAnn b))
-> a
-> WithComments a
everywhereMEpAnnsBackwards =
everywhereMEpAnnsInOrder (flip compareEpaByEndPosition)
-- | 'everywhereM' but applies the given function to EPAs in order
-- specified by the given ordering function.
everywhereMEpAnnsInOrder ::
Data a
=> (forall b c. EpAnn b -> EpAnn c -> Ordering)
-> (forall b. EpAnn b -> WithComments (EpAnn b))
-> a
-> WithComments a
everywhereMEpAnnsInOrder cmp f hm =
collectEpAnnsInOrderEverywhereMTraverses
>>= applyFunctionInOrderEpAnnEndPositions
>>= putModifiedEpAnnsToModule
where
collectEpAnnsInOrderEverywhereMTraverses
-- This function uses 'everywhereM' to collect 'EpAnn's because they
-- should be collected in the same order as 'putModifiedEpAnnsToModule'
-- puts them to the AST.
= reverse <$> execStateT (everywhereM collectEpAnnsST hm) []
where
collectEpAnnsST x = do
modify $ collectEpAnns x
pure x
collectEpAnns ::
forall a. Typeable a
=> a
-> ([Wrapper] -> [Wrapper])
collectEpAnns x
-- If 'a' is 'EpAnn b' ('b' can be any type), wrap 'x' with a 'Wrapper'.
| App g _ <- typeRep @a
, Just HRefl <- eqTypeRep g (typeRep @EpAnn) = (Wrapper x :)
| otherwise = id
applyFunctionInOrderEpAnnEndPositions ::
[Wrapper]
-> WithComments [(Int, Wrapper)] -- ^ The first element of the tuple
-- indicates how many 'Wrapper's were there before 'everywhereM'
-- accessed the second element.
applyFunctionInOrderEpAnnEndPositions anns =
forM sorted $ \(i, Wrapper x) -> do
x' <- f x
pure (i, Wrapper x')
where
indexed = zip [0 :: Int ..] anns
sorted = sortBy (\(_, Wrapper a) (_, Wrapper b) -> cmp a b) indexed
putModifiedEpAnnsToModule anns = evalStateT (everywhereM setEpAnn hm) [0 ..]
where
setEpAnn ::
forall a. Typeable a
=> a
-> StateT [Int] WithComments a
setEpAnn x
-- This guard arm checks if 'a' is 'EpAnn b' ('b' can be any type).
| App g g' <- typeRep @a
, Just HRefl <- eqTypeRep g (typeRep @EpAnn) = do
i <- gets head
modify tail
case lookup i anns of
Just (Wrapper y)
| App _ h <- typeOf y
, Just HRefl <- eqTypeRep g' h -> pure y
_ -> error "Unmatches"
| otherwise = pure x
-- | This function sorts comments by its location.
sortCommentsByLocation :: [LEpaComment] -> [LEpaComment]
sortCommentsByLocation = sortBy (compare `on` anchor . getLoc)
-- | This function compares given EPAs by their end positions.
compareEpaByEndPosition :: EpAnn a -> EpAnn b -> Ordering
compareEpaByEndPosition (EpAnn a _ _) (EpAnn b _ _) =
on compare (realSrcSpanEnd . anchor) a b
compareEpaByEndPosition EpAnnNotUsed EpAnnNotUsed = EQ
compareEpaByEndPosition _ EpAnnNotUsed = GT
compareEpaByEndPosition EpAnnNotUsed _ = LT