apply-refact-0.9.3.0: src/Refact/Utils.hs
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ViewPatterns #-}
module Refact.Utils
( -- * Synonyms
Module,
Stmt,
Expr,
Decl,
Name,
Pat,
Type,
Import,
FunBind,
AnnKeyMap,
pattern RealSrcLoc',
pattern RealSrcSpan',
-- * Monad
M,
-- * Utility
mergeAnns,
modifyAnnKey,
replaceAnnKey,
getAnnSpan,
toGhcSrcSpan,
toGhcSrcSpan',
annSpanToSrcSpan,
srcSpanToAnnSpan,
setAnnSpanFile,
setSrcSpanFile,
setRealSrcSpanFile,
findParent,
foldAnnKey,
)
where
import Control.Monad.Trans.State (StateT, gets, modify)
import Data.Bifunctor (bimap)
import Data.Data
( Data (gmapQi, toConstr),
Proxy (..),
splitTyConApp,
typeOf,
typeRep,
typeRepTyCon,
)
import Data.Generics.Schemes (something)
import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe, isJust)
import Data.Typeable (Typeable, eqT, (:~:) (Refl))
import qualified GHC
import Language.Haskell.GHC.ExactPrint
import Language.Haskell.GHC.ExactPrint.Types
import Refact.Compat
( AnnKeyMap,
AnnKeywordId (..),
FastString,
FunBind,
Module,
annSpanToSrcSpan,
mkFastString,
setAnnSpanFile,
setRealSrcSpanFile,
setSrcSpanFile,
srcSpanToAnnSpan,
pattern RealSrcLoc',
pattern RealSrcSpan',
)
import qualified Refact.Types as R
import Unsafe.Coerce (unsafeCoerce)
-- Types
type M a = StateT (Anns, AnnKeyMap) IO a
type Expr = GHC.Located (GHC.HsExpr GHC.GhcPs)
type Type = GHC.Located (GHC.HsType GHC.GhcPs)
type Decl = GHC.Located (GHC.HsDecl GHC.GhcPs)
type Pat = GHC.Located (GHC.Pat GHC.GhcPs)
type Name = GHC.Located GHC.RdrName
type Stmt = GHC.ExprLStmt GHC.GhcPs
type Import = GHC.LImportDecl GHC.GhcPs
-- | Replaces an old expression with a new expression
--
-- Note that usually, new, inp and parent are all the same.
replace ::
AnnKey -> -- The thing we are replacing
AnnKey -> -- The thing which has the annotations we need for the new thing
AnnKey -> -- The thing which is going to be inserted
AnnKey -> -- The "parent", the largest thing which has he same SrcSpan
-- Usually the same as inp and new
Anns ->
Maybe Anns
replace old new inp parent anns = do
oldan <- Map.lookup old anns
newan <- Map.lookup new anns
oldDelta <- annEntryDelta <$> Map.lookup parent anns
return $ Map.insert inp (combine oldDelta new oldan newan) anns
combine :: DeltaPos -> AnnKey -> Annotation -> Annotation -> Annotation
combine oldDelta newkey oldann newann =
Ann
{ annEntryDelta = newEntryDelta,
annPriorComments = annPriorComments oldann ++ annPriorComments newann,
annFollowingComments = annFollowingComments oldann ++ annFollowingComments newann,
annsDP = removeComma (annsDP newann) ++ extraComma (annsDP oldann),
annSortKey = annSortKey newann,
annCapturedSpan = annCapturedSpan newann
}
where
-- Get rid of structural information when replacing, we assume that the
-- structural information is already there in the new expression.
removeComma =
filter
( \(kw, _) -> case kw of
G AnnComma
| AnnKey _ (CN "ArithSeq") <- newkey -> True
| otherwise -> False
AnnSemiSep -> False
_ -> True
)
-- Make sure to keep structural information in the template.
extraComma [] = []
extraComma (last -> x) = case fst x of
G AnnComma -> [x]
AnnSemiSep -> [x]
G AnnSemi -> [x]
_ -> []
-- Keep the same delta if moving onto a new row
newEntryDelta
| deltaRow oldDelta > 0 = oldDelta
| otherwise = annEntryDelta oldann
-- | A parent in this case is an element which has the same SrcSpan
findParent :: Data a => AnnSpan -> Anns -> a -> Maybe AnnKey
findParent ss as = something (findParentWorker ss as)
-- Note that a parent must also have an annotation.
findParentWorker ::
forall a.
(Data a) =>
AnnSpan ->
Anns ->
a ->
Maybe AnnKey
findParentWorker oldSS as a
| con == typeRepTyCon (typeRep (Proxy :: Proxy (GHC.Located GHC.RdrName))) && x == typeRep (Proxy :: Proxy AnnSpan) =
if ss == oldSS
&& isJust (Map.lookup (AnnKey ss cn) as)
then Just $ AnnKey ss cn
else Nothing
| otherwise = Nothing
where
(con, ~[x, _]) = splitTyConApp (typeOf a)
ss :: AnnSpan
ss = gmapQi 0 unsafeCoerce a
cn = gmapQi 1 (CN . show . toConstr) a
getAnnSpan :: forall a. GHC.Located a -> AnnSpan
getAnnSpan = srcSpanToAnnSpan . GHC.getLoc
-- | Perform the necessary adjustments to annotations when replacing
-- one Located thing with another Located thing.
--
-- For example, this function will ensure the correct relative position and
-- make sure that any trailing semi colons or commas are transferred.
modifyAnnKey ::
(Data old, Data new, Data mod) =>
mod ->
GHC.Located old ->
GHC.Located new ->
M (GHC.Located new)
modifyAnnKey m e1 e2 = do
as <- gets fst
let parentKey = fromMaybe (mkAnnKey e2) (findParent (getAnnSpan e2) as m)
e2
<$ modify
( bimap
( dropContextParens e1 e2
. recoverBackquotes e1 e2
. replaceAnnKey (mkAnnKey e1) (mkAnnKey e2) (mkAnnKey e2) parentKey
)
(Map.insertWith (++) (mkAnnKey e1) [mkAnnKey e2])
)
-- | When parens are removed for the entire context, e.g.,
--
-- @
-- - f :: (HasCallStack) => ...
-- + f :: HasCallStack => ...
-- @
--
-- We need to remove the `AnnOpenP` and `AnnCloseP` from the corresponding `Annotation`.
dropContextParens ::
forall old new.
(Typeable old, Typeable new) =>
GHC.Located old ->
GHC.Located new ->
Anns ->
Anns
dropContextParens old new anns
| Just Refl <- eqT @old @(GHC.HsType GHC.GhcPs),
Just Refl <- eqT @new @(GHC.HsType GHC.GhcPs),
isParTy old,
not (isParTy new),
Just annOld <- Map.lookup key anns,
(G AnnOpenP, _) : (G AnnCloseP, _) : rest <- annsDP annOld =
Map.adjust (\x -> x {annsDP = rest}) key anns
| otherwise = anns
where
key = AnnKey (getAnnSpan old) (CN "(:)")
isParTy = \case (GHC.L _ GHC.HsParTy {}) -> True; _ -> False
-- | When the template contains a backquoted substitution variable, but the substitute
-- is not backquoted, we must add the corresponding 'GHC.AnnBackQuote's.
--
-- See tests/examples/Backquotes.hs for an example.
recoverBackquotes :: GHC.Located old -> GHC.Located new -> Anns -> Anns
recoverBackquotes (getAnnSpan -> old) (getAnnSpan -> new) anns
| Just annOld <- Map.lookup (AnnKey old (CN "Unqual")) anns,
( (G AnnBackquote, DP (i, j))
: rest@( (G AnnVal, _)
: (G AnnBackquote, _)
: _
)
) <-
annsDP annOld =
let f annNew = case annsDP annNew of
[(G AnnVal, DP (i', j'))] ->
annNew {annsDP = (G AnnBackquote, DP (i + i', j + j')) : rest}
_ -> annNew
in Map.adjust f (AnnKey new (CN "Unqual")) anns
| otherwise = anns
-- | Lower level version of @modifyAnnKey@
replaceAnnKey :: AnnKey -> AnnKey -> AnnKey -> AnnKey -> Anns -> Anns
replaceAnnKey old new inp deltainfo a =
fromMaybe a (replace old new inp deltainfo a)
-- | Convert a @Refact.Types.SrcSpan@ to a @SrcLoc.SrcSpan@
toGhcSrcSpan :: FilePath -> R.SrcSpan -> GHC.SrcSpan
toGhcSrcSpan = toGhcSrcSpan' . mkFastString
-- | Convert a @Refact.Types.SrcSpan@ to a @SrcLoc.SrcSpan@
toGhcSrcSpan' :: FastString -> R.SrcSpan -> GHC.SrcSpan
toGhcSrcSpan' file R.SrcSpan {..} = GHC.mkSrcSpan (f startLine startCol) (f endLine endCol)
where
f = GHC.mkSrcLoc file
foldAnnKey ::
forall a.
(AnnKey -> a) ->
(GHC.RealSrcSpan -> AnnConName -> a) ->
AnnKey ->
a
foldAnnKey f g key@(AnnKey (annSpanToSrcSpan -> ss) con) = case ss of
RealSrcSpan' r -> g r con
_ -> f key