packages feed

retrie-2.0.0: Retrie/ExactPrint.hs

-- Copyright (c) 2025 Andrew Farmer
-- Copyright (c) 2020-2024 Facebook, Inc. and its affiliates.
--
-- This source code is licensed under the MIT license found in the
-- LICENSE file in the root directory of this source tree.
--
{-# LANGUAGE CPP #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns #-}
#if __GLASGOW_HASKELL__ < 912
#else
{-# OPTIONS_GHC -Wno-orphans #-}
#endif
-- | Provides consistent interface with ghc-exactprint.
module Retrie.ExactPrint
  ( -- * Fixity re-association
    fix
    -- * Parsers
  , Parsers.LibDir
  , parseContent
  , parseContentNoFixity
  , parseDecl
  , parseExpr
  , parseImports
  , parsePattern
  , parseStmt
  , parseType
    -- * Primitive Transformations
  , addAllAnnsT
  -- , cloneT
  -- , setEntryDPT
  , swapEntryDPT
  , transferAnnsT
  , transferEntryAnnsT
  , setTrailingAnns
  , trailingAnns
  , stripOuterAnns
    -- * Utils
  , debugDump
  , debugParse
  , hasComments
  , isComma
    -- * Annotated AST
  , module Retrie.ExactPrint.Annotated
    -- * ghc-exactprint re-exports
  , module Language.Haskell.GHC.ExactPrint
  , module Language.Haskell.GHC.ExactPrint.Types
  , module Language.Haskell.GHC.ExactPrint.Utils
  , module Language.Haskell.GHC.ExactPrint.Transform
  ) where

import Control.Exception
import Control.Monad
import Control.Monad.State.Lazy
import Data.List (transpose)
import Text.Printf

import Language.Haskell.GHC.ExactPrint hiding
  ( d1
#if __GLASGOW_HASKELL__ < 912
  , m1
  , mn
#endif
  , setEntryDP
  , transferEntryDP
  )
import Language.Haskell.GHC.ExactPrint.Utils hiding
  ( rs
  )
import qualified Language.Haskell.GHC.ExactPrint.Parsers as Parsers
import Language.Haskell.GHC.ExactPrint.Types
  ( showGhc
  )
import Language.Haskell.GHC.ExactPrint.Transform hiding
  ( d1
#if __GLASGOW_HASKELL__ < 912
  , m1
  , mn
#endif
  )

import Retrie.ExactPrint.Annotated
import Retrie.Fixity
import Retrie.GHC
import Retrie.SYB hiding (ext1)

import GHC.Stack

#if __GLASGOW_HASKELL__ < 912
#else
import Data.Default
import Control.Applicative

instance Monoid AnnListItem where
  mempty = AnnListItem []

instance Default AnnListItem where
  def = AnnListItem []

instance Monoid (EpToken s) where
  mempty = NoEpTok
  mappend = (<>)

instance Semigroup (EpToken s) where
  _ <> _ = NoEpTok

instance Semigroup NameAnn where
  _ <> b = b -- Right biased for now

instance Monoid NameAnn where
  mempty = NameAnnTrailing []

instance (Monoid a) => Monoid (AnnList a) where
  mempty = AnnList Nothing ListNone [] mempty []

instance (Semigroup a) => Semigroup (AnnList a) where
  (AnnList a1 b1 s1 r1 t1) <> (AnnList a2 b2 s2 r2 t2) =
    AnnList (a1 <|> a2) (if b1 == ListNone then b2 else b1) (s1 <> s2) (r1 <> r2) (t1 <> t2)
#endif

-- Fixity traversal -----------------------------------------------------------

-- | Re-associate AST using given 'FixityEnv'. (The GHC parser has no knowledge
-- of operator fixity, because that requires running the renamer, so it parses
-- all operators as left-associated.)
fix :: (Data ast, MonadIO m) => FixityEnv -> ast -> TransformT m ast
fix env = fixAssociativity >=> fixEntryDP
  where
    fixAssociativity = everywhereM (mkM (fixOneExpr env) `extM` fixOnePat env)
    fixEntryDP = everywhereM (mkM fixOneEntryExpr `extM` fixOneEntryPat)

-- Should (x op1 y) op2 z be reassociated as x op1 (y op2 z)?
associatesRight :: Fixity -> Fixity -> Bool
#if __GLASGOW_HASKELL__ < 912
associatesRight (Fixity _ p1 a1) (Fixity _ p2 _a2) =
#else
associatesRight (Fixity p1 a1) (Fixity p2 _a2) =
#endif
  p2 > p1 || p1 == p2 && a1 == InfixR

-- We know GHC produces left-associated chains, so 'z' is never an
-- operator application. We also know that this will be applied bottom-up
-- by 'everywhere', so we can assume the children are already fixed.
fixOneExpr
  :: MonadIO m
  => FixityEnv
  -> LHsExpr GhcPs
  -> TransformT m (LHsExpr GhcPs)
fixOneExpr env (L l2 (OpApp x2 (L _ (OpApp x1 x op1 y)) op2 z))
  | associatesRight (lookupOp op1 env) (lookupOp op2 env) = do
    let sp = combineSrcSpans (getLocA y) (getLocA z)
        ap2' = L (freshAnn sp) $ OpApp x2 y op2 z
    rhs <- fixOneExpr env ap2'
    return $ L l2 $ OpApp x1 x op1 rhs
fixOneExpr _ e = return e

fixOnePat :: Monad m => FixityEnv -> LPat GhcPs -> TransformT m (LPat GhcPs)
fixOnePat env (dLPat -> Just (L l2 (ConPat ext2 op2 (InfixCon (dLPat -> Just (L _ (ConPat ext1 op1 (InfixCon x y)))) z))))
  | associatesRight (lookupOpRdrName op1 env) (lookupOpRdrName op2 env) = do
    let sp = combineSrcSpans (getLocA y) (getLocA z)
        ap2' = L (freshAnn sp) $ ConPat ext2 op2 (InfixCon y z)
    rhs <- fixOnePat env ap2'
    return $ L l2 $ ConPat ext1 op1 (InfixCon x rhs)
fixOnePat _ e = return e

#if __GLASGOW_HASKELL__ < 912
freshAnn :: Monoid an => SrcSpan -> SrcAnn an
freshAnn = noAnnSrcSpanDP0
#else
freshAnn :: NoAnn an => SrcSpan -> EpAnn an
freshAnn sp = EpAnn (EpaDelta sp (SameLine 0) []) noAnn emptyComments
#endif

-- Move leading whitespace from the left child of an operator application
-- to the application itself. We need this so we have correct offsets when
-- substituting into patterns and don't end up with extra leading spaces.
-- We can assume it is run bottom-up, and that precedence is already fixed.
fixOneEntry
  :: (MonadIO m, Data a)
  => LocatedA a -- ^ Overall application
  -> LocatedA a -- ^ Left child
  -> TransformT m (LocatedA a, LocatedA a)
fixOneEntry e x = do
  -- We assume that ghc-exactprint has converted all Anchor's to use their delta variants.
  -- Get the dp for the x component
  let xdp = entryDP x
  let xr = getDeltaLine xdp
  let xc = deltaColumn xdp
  -- Get the dp for the e component
  let edp = entryDP e
  let er = getDeltaLine edp
  let ec = deltaColumn edp
  case xdp of
    SameLine _n -> do
      -- lift $ liftIO $ debugPrint Loud "fixOneEntry:(xdp,edp)="  [showAst (xdp,edp)]
      -- lift $ liftIO $ debugPrint Loud "fixOneEntry:(dpx,dpe)="  [showAst ((deltaPos er (xc + ec)),(deltaPos xr 0))]
      -- lift $ liftIO $ debugPrint Loud "fixOneEntry:e'="  [showAst e]
      -- lift $ liftIO $ debugPrint Loud "fixOneEntry:e'="  [showAst (setEntryDP e (deltaPos er (xc + ec)))]
#if __GLASGOW_HASKELL__ < 912
      return ( setEntryDP e (deltaPos er (xc + ec))
             , setEntryDP x (deltaPos xr 0))
#else
      -- In GHC 9.12+, setEntryDP applies the delta to the first comment if present,
      -- which can corrupt spacing. Skip adjustment when either e or x has comments
      -- directly attached to them (not in subtree), as setEntryDP will be called
      -- on these specific nodes.
      if hasComments e || hasComments x
        then return (e, x)
        else return ( setEntryDP e (deltaPos er (xc + ec))
                    , setEntryDP x (deltaPos xr 0))
#endif
    _ -> return (e,x)

-- TODO: move this somewhere more appropriate
entryDP :: LocatedA a -> DeltaPos
#if __GLASGOW_HASKELL__ < 912
entryDP (L (SrcSpanAnn EpAnnNotUsed _) _) = SameLine 1
entryDP (L (SrcSpanAnn (EpAnn anc _ _) _) _)
  = case anchor_op anc of
      UnchangedAnchor -> SameLine 1
      MovedAnchor dp -> dp
#else
entryDP (L (EpAnn anc _ _) _)
  = case anc of
      EpaSpan _ -> SameLine 1
      EpaDelta _ dp _ -> dp
#endif


fixOneEntryExpr :: MonadIO m => LHsExpr GhcPs -> TransformT m (LHsExpr GhcPs)
fixOneEntryExpr e@(L _ (OpApp a x b c)) = do
  -- lift $ liftIO $ debugPrint Loud "fixOneEntryExpr:(e,x)="  [showAst (e,x)]
  (e',x') <- fixOneEntry e x
  -- lift $ liftIO $ debugPrint Loud "fixOneEntryExpr:(e',x')="  [showAst (e',x')]
  -- lift $ liftIO $ debugPrint Loud "fixOneEntryExpr:returning="  [showAst (L (getLoc e') (OpApp a x' b c))]
  return (L (getLoc e') (OpApp a x' b c))
fixOneEntryExpr e = return e

fixOneEntryPat :: MonadIO m => LPat GhcPs -> TransformT m (LPat GhcPs)
fixOneEntryPat pat
  | Just p@(L _ (ConPat a b (InfixCon x c))) <- dLPat pat = do
    (p', x') <- fixOneEntry p (dLPatUnsafe x)
    return (cLPat $ (L (getLoc p') (ConPat a b (InfixCon x' c))))
  | otherwise = return pat

-------------------------------------------------------------------------------


#if __GLASGOW_HASKELL__ < 912
-- Swap entryDP and prior comments between the two args
swapEntryDPT
  :: (Data a, Data b, Monad m, Monoid a1, Monoid a2, Typeable a1, Typeable a2)
  => LocatedAn a1 a -> LocatedAn a2 b -> TransformT m (LocatedAn a1 a, LocatedAn a2 b)
swapEntryDPT a b = do
  b' <- transferEntryDP a b
  a' <- transferEntryDP b a
  return (a',b')
#else
swapEntryDPT
  :: (Monad m, Typeable t2, Typeable t1)
  => LocatedAn t1 a
  -> LocatedAn t2 b
  -> m (LocatedAn t1 a, LocatedAn t2 b)
swapEntryDPT a b = return (transferEntryDP b a, transferEntryDP a b)
#endif

-------------------------------------------------------------------------------

-- Compatibility module with ghc-exactprint

parseContentNoFixity :: Parsers.LibDir -> FilePath -> String -> IO AnnotatedModule
parseContentNoFixity libdir fp str = join $ Parsers.withDynFlags libdir $ \dflags -> do
  r <- Parsers.parseModuleFromString libdir fp str
  case r of
    Left msg -> do
      fail $ showSDoc dflags $ ppr msg
    Right m -> return $ unsafeMkA (makeDeltaAst m) 0

parseContent :: Parsers.LibDir -> FixityEnv -> FilePath -> String -> IO AnnotatedModule
parseContent libdir fixities fp =
  parseContentNoFixity libdir fp >=> (`transformA` fix fixities)

-- | Parse import statements. Each string must be a full import statement,
-- including the keyword 'import'. Supports full import syntax.
parseImports :: Parsers.LibDir -> [String] -> IO AnnotatedImports
parseImports _      []      = return mempty
parseImports libdir imports = do
  -- imports start on second line, so delta offsets are correct
  am <- parseContentNoFixity libdir "parseImports" $ "\n" ++ unlines imports
  ais <- transformA am $ pure . hsmodImports . unLoc
  return $ trimA ais

-- | Parse a top-level 'HsDecl'.
parseDecl :: Parsers.LibDir -> String -> IO AnnotatedHsDecl
parseDecl libdir str = parseHelper libdir "parseDecl" Parsers.parseDecl str

-- | Parse a 'HsExpr'.
parseExpr :: Parsers.LibDir -> String -> IO AnnotatedHsExpr
parseExpr libdir str = parseHelper libdir "parseExpr" Parsers.parseExpr str

-- | Parse a 'Pat'.
parsePattern :: Parsers.LibDir -> String -> IO AnnotatedPat
-- parsePattern libdir str = parseHelper libdir "parsePattern" p str
--   where
--     p flags fp str' = fmap dLPatUnsafe <$> Parsers.parsePattern flags fp str'
parsePattern libdir str = parseHelper libdir "parsePattern" Parsers.parsePattern str

-- | Parse a 'Stmt'.
parseStmt :: Parsers.LibDir -> String -> IO AnnotatedStmt
parseStmt libdir str = do
  -- debugPrint Loud "parseStmt:for" [str]
  res <- parseHelper libdir "parseStmt" Parsers.parseStmt str
  return (setEntryDPA res (DifferentLine 1 0))


-- | Parse a 'HsType'.
parseType :: Parsers.LibDir -> String -> IO AnnotatedHsType
parseType libdir str = parseHelper libdir "parseType" Parsers.parseType str

parseHelper :: (ExactPrint a)
  => Parsers.LibDir -> FilePath -> Parsers.Parser a -> String -> IO (Annotated a)
parseHelper libdir fp parser str = join $ Parsers.withDynFlags libdir $ \dflags ->
  case parser dflags fp str of
    Left msg -> throwIO $ ErrorCall (showSDoc dflags $ ppr msg)
    Right x -> return $ unsafeMkA (makeDeltaAst x) 0

-- type Parser a = GHC.DynFlags -> FilePath -> String -> ParseResult a


-------------------------------------------------------------------------------

debugDump :: (Data a, ExactPrint a) => Annotated a -> IO ()
debugDump ax = do
  let
    str = printA ax
    maxCol = maximum $ map length $ lines str
    (tens, ones) =
      case transpose [printf "%2d" i | i <- [1 .. maxCol]] of
        [ts, os] -> (ts, os)
        _ -> ("", "")
  -- putStrLn $ unlines
  --   [ show k ++ "\n  " ++ show v | (k,v) <- M.toList (annsA ax) ]
  putStrLn tens
  putStrLn ones
  putStrLn str
  putStrLn "------------------------------------"
  putStrLn $ showAstA ax
  putStrLn "------------------------------------"

-- cloneT :: (Data a, Typeable a, Monad m) => a -> TransformT m a
-- cloneT e = getAnnsT >>= flip graftT e

transferEntryAnnsT
  :: (HasCallStack, Data a, Data b, Monad m)
  => LocatedA a             -- from
  -> LocatedA b             -- to
  -> TransformT m (LocatedA b)
transferEntryAnnsT a b =
  return
    $ setTrailingAnns (trailingAnns a)
    $ pinPriorComments
    $ prependPriorComments (priorCommentsOf a)
    $ setEntryDelta (getEntryDP a) b

setEntryDelta :: DeltaPos -> LocatedA a -> LocatedA a
#if __GLASGOW_HASKELL__ < 912
-- always give the delta to the first prior comment
setEntryDelta dp (L (SrcSpanAnn (EpAnn anc@(Anchor _ (MovedAnchor _)) an cs) l) x)
  | (L ca c : rest) <- priorComments cs =
      let c' = L (Anchor (anchor ca) (MovedAnchor dp)) c
      in L (SrcSpanAnn (EpAnn anc an (setPriorComments cs (c' : rest))) l) x
#endif
setEntryDelta dp x = setEntryDP x dp

addAllAnnsT
#if __GLASGOW_HASKELL__ < 912
  :: (HasCallStack, Monoid an, Data a, Data b, MonadIO m, Typeable an)
  => LocatedAn an a -> LocatedAn an b -> TransformT m (LocatedAn an b)
addAllAnnsT a b = do
  -- AZ: to start with, just transfer the entry DP from a to b
  transferEntryDP a b
#else
  :: (HasCallStack, Data a, Data b, Monad m, Typeable an)
  => LocatedAn an a -> LocatedAn an b -> TransformT m (LocatedAn an b)
addAllAnnsT a b = return $ transferEntryDP a b
#endif

setTrailingAnns :: [TrailingAnn] -> LocatedA a -> LocatedA a
#if __GLASGOW_HASKELL__ < 912
setTrailingAnns [] x@(L (SrcSpanAnn EpAnnNotUsed _) _) = x
setTrailingAnns ts (L (SrcSpanAnn EpAnnNotUsed l) x) =
  L (SrcSpanAnn (EpAnn (spanAsAnchor l) (AnnListItem ts) emptyComments) l) x
setTrailingAnns ts (L (SrcSpanAnn (EpAnn anc _ cs) l) x) =
  L (SrcSpanAnn (EpAnn anc (AnnListItem ts) cs) l) x
#else
setTrailingAnns ts (L (EpAnn anc _ cs) x) = L (EpAnn anc (AnnListItem ts) cs) x
#endif

trailingAnns :: LocatedA a -> [TrailingAnn]
#if __GLASGOW_HASKELL__ < 912
trailingAnns (L (SrcSpanAnn (EpAnn _ (AnnListItem ts) _) _) _) = ts
trailingAnns _ = []
#else
trailingAnns (L (EpAnn _ (AnnListItem ts) _) _) = ts
#endif

priorCommentsOf :: LocatedA a -> [LEpaComment]
#if __GLASGOW_HASKELL__ < 912
priorCommentsOf (L (SrcSpanAnn EpAnnNotUsed _) _) = []
priorCommentsOf (L (SrcSpanAnn (EpAnn _ _ cs) _) _) = priorComments cs
#else
priorCommentsOf (L (EpAnn _ _ cs) _) = priorComments cs
#endif

prependPriorComments :: [LEpaComment] -> LocatedA a -> LocatedA a
prependPriorComments [] x = x
#if __GLASGOW_HASKELL__ < 912
prependPriorComments new (L l x) =
  L (setCommentsSrcAnn l (EpaComments new <> epAnnComments (ann l))) x
#else
prependPriorComments new (L l x) =
  L (setCommentsEpAnn l (EpaComments new <> epAnnComments l)) x
#endif

pinPriorComments :: LocatedA a -> LocatedA a
#if __GLASGOW_HASKELL__ < 912
-- exactprint sorts prior comments by span and holds back any that
-- start after the node's anchor, pin them to the nodes anchor so order is
-- consistent across versions.
pinPriorComments (L (SrcSpanAnn (EpAnn anc an cs) l) x) =
  L (SrcSpanAnn (EpAnn anc an (setPriorComments cs (map pin (priorComments cs)))) l) x
  where
    pin (L (Anchor _ op) c) = L (Anchor (anchor anc) op) c
#endif
pinPriorComments x = x

-- | Drop anything that prints outside the node's span.
stripOuterAnns :: LocatedA a -> LocatedA a
stripOuterAnns (L an x) = L (freshAnn (locA an)) x

isComma :: TrailingAnn -> Bool
isComma (AddCommaAnn _) = True
isComma _ = False

hasComments :: LocatedAn an a -> Bool
#if __GLASGOW_HASKELL__ < 912
hasComments (L (SrcSpanAnn EpAnnNotUsed _) _) = False
hasComments (L (SrcSpanAnn (EpAnn _ _ cs) _) _) =
#else
hasComments (L (EpAnn _ _ cs) _) =
#endif
  case cs of
    EpaComments [] -> False
    EpaCommentsBalanced [] [] -> False
    _ -> True

transferAnnsT
  :: (Data a, Data b, Monad m)
  => (TrailingAnn -> Bool)      -- transfer Anns matching predicate
  -> LocatedA a                 -- from
  -> LocatedA b                 -- to
  -> TransformT m (LocatedA b)
#if __GLASGOW_HASKELL__ < 912
transferAnnsT _ (L (SrcSpanAnn EpAnnNotUsed _) _) b = return b
transferAnnsT p (L (SrcSpanAnn (EpAnn _ (AnnListItem ts) _) _) _) (L (SrcSpanAnn an lb) b) = do
  let ps = filter p ts
  let an' = case an of
        EpAnnNotUsed -> EpAnn (spanAsAnchor lb) (AnnListItem ps) emptyComments
        EpAnn ancb (AnnListItem tsb) csb -> EpAnn ancb (AnnListItem (tsb++ps)) csb
  return (L (SrcSpanAnn an' lb) b)
#else
transferAnnsT p (L (EpAnn _ (AnnListItem ts) _) _) (L (EpAnn ancb (AnnListItem tsb) csb) b) =
  return $ L (EpAnn ancb (AnnListItem (tsb ++ filter p ts)) csb) b
#endif


-- -- | 'Transform' monad version of 'setEntryDP',
-- --   which sets the entry 'DeltaPos' for an annotation.
-- setEntryDPT
--   :: (Data a, Monad m)
--   => Located a -> DeltaPos -> TransformT m ()
-- setEntryDPT ast dp = do
--   modifyAnnsT (setEntryDP ast dp)

-- -- | Set the true entry 'DeltaPos' from the annotation of a
-- --   given AST element.
-- setEntryDP :: Data a => Located a -> DeltaPos -> Anns -> Anns
-- --  The setEntryDP that comes with exactprint does some really confusing
-- --  entry math around comments that I'm unconvinced is either correct or useful.
-- setEntryDP x dp anns = M.alter (Just . f . fromMaybe annNone) k anns
--   where
--     k = mkAnnKey x
--     f ann = case annPriorComments ann of
--               []       -> ann { annEntryDelta = dp }
--               (c,_):cs -> ann { annPriorComments = (c,dp):cs }

-- Useful for figuring out what annotations should be on something.
-- If you don't care about fixities, pass 'mempty' as the FixityEnv.
-- String should be the entire module contents.
debugParse :: Parsers.LibDir -> FixityEnv -> String -> IO ()
debugParse libdir fixityEnv s = do
  writeFile "debug.txt" s
  r <- parseModule libdir "debug.txt"
  case r of
    Left _ -> putStrLn "parse failed"
    Right modl -> do
      let m = unsafeMkA (makeDeltaAst modl) 0
      putStrLn "parseModule"
      debugDump m
      void $ transformDebug m
  where
    transformDebug =
      run "fixOneExpr D.def" (fixOneExpr fixityEnv)
        >=> run "fixOnePat D.def" (fixOnePat fixityEnv)
        >=> run "fixOneEntryExpr" fixOneEntryExpr
        >=> run "fixOneEntryPat" fixOneEntryPat

    run wat f m = do
      putStrLn wat
      m' <- transformA m (everywhereM (mkM f))
      debugDump m'
      return m'