packages feed

ormolu-0.9.0.0: src/Ormolu/Printer/Meat/Common.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ViewPatterns #-}

-- | Rendering of commonly useful bits.
module Ormolu.Printer.Meat.Common
  ( FamilyStyle (..),
    p_hsmodName,
    p_ieWrappedName,
    p_rdrName,
    p_qualName,
    p_infixDefHelper,
    p_hsDoc,
    p_hsDocInline,
    multiLineIfDocumented,
    switchLayoutDocumented,
    hasLineHaddocks,
    p_hsDocName,
    p_sourceText,
    p_namespaceSpec,
    p_hsMultAnn,
  )
where

import Control.Monad
import Data.Choice (Choice, pattern Is, pattern Isn't, pattern With)
import Data.Choice qualified as Choice
import Data.Data (Data)
import Data.Generics.Schemes (listify)
import Data.List.NonEmpty qualified as NE
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Data.FastString
import GHC.Hs.Binds
import GHC.Hs.Doc
import GHC.Hs.Extension (GhcPs)
import GHC.Hs.ImpExp
import GHC.Hs.Type
import GHC.LanguageExtensions.Type (Extension (..))
import GHC.Parser.Annotation
import GHC.Types.Name.Occurrence (OccName (..), occNameString)
import GHC.Types.Name.Reader
import GHC.Types.SourceText
import GHC.Types.SrcLoc
import Language.Haskell.Syntax.Module.Name
import Ormolu.Config (SourceType (..))
import Ormolu.Parser.CommentStream (Comment, isMultilineComment, unComment)
import Ormolu.Printer.Combinators
import Ormolu.Utils

-- | Data and type family style.
data FamilyStyle
  = -- | Declarations in type classes
    Associated
  | -- | Top-level declarations
    Free

-- | Output the name of the module-like entity, preceded by the correct
-- prefix (@module@ or @signature@).
p_hsmodName :: ModuleName -> R ()
p_hsmodName mname = do
  sourceType <- askSourceType
  txt $ case sourceType of
    ModuleSource -> "module"
    SignatureSource -> "signature"
  space
  atom mname

p_ieWrappedName :: IEWrappedName GhcPs -> R ()
p_ieWrappedName = \case
  IEName _ x -> p_rdrName x
  IEDefault _ x -> do
    txt "default"
    space
    p_rdrName x
  IEPattern _ x -> do
    txt "pattern"
    space
    p_rdrName x
  IEType _ x -> do
    txt "type"
    space
    p_rdrName x
  IEData _ x -> do
    txt "data"
    space
    p_rdrName x

-- | Render a @'LocatedN' 'RdrName'@.
p_rdrName :: LocatedN RdrName -> R ()
p_rdrName l = located l $ \x -> do
  unboxedSums <- isExtensionEnabled UnboxedSums
  let wrapper EpAnn {anns} = case anns of
        NameAnnQuote {nann_quoted} -> tickPrefix . wrapper nann_quoted
        NameAnn {nann_adornment = NameParens {}} ->
          parens N . handleUnboxedSumsAndHashInteraction
        NameAnn {nann_adornment = NameBackquotes {}} -> backticks
        -- whether the `->` identifier is parenthesized
        NameAnnRArrow {nann_mopen = Just _} -> parens N
        -- special case for unboxed unit tuples
        NameAnnOnly {nann_adornment = NameParensHash {}} -> const $ txt "(# #)"
        -- An empty list reaches the printer as a name, not as a list, so
        -- this is the only place a comment written between its brackets can
        -- be given something to attach to.
        NameAnnOnly {nann_adornment = NameSquare open _} ->
          const $ brackets N (locatedEmpty (getEpTokenSrcSpan open))
        _ -> id

      -- When UnboxedSums is enabled, `(#` is a single lexeme, so we have to
      -- insert spaces when we have a parenthesized operator starting with `#`.
      handleUnboxedSumsAndHashInteraction
        | unboxedSums,
          -- Qualified names do not start with a `#`.
          Unqual (occNameString -> '#' : _) <- x =
            \y -> space *> y <* space
        | otherwise = id

  wrapper (getLoc l) $ case x of
    Unqual occName ->
      atom occName
    Qual mname occName ->
      p_qualName mname occName
    Orig _ occName ->
      -- This is used when GHC generates code that will be fed into
      -- the renamer (e.g. from deriving clauses), but where we want
      -- to say that something comes from a given module that is not
      -- specified in the source code, e.g. @Prelude.map@.
      --
      -- My current understanding is that the provided module name
      -- serves no purpose for us and can be safely ignored.
      atom occName
    Exact name ->
      atom name
  where
    tickPrefix y = txt "'" *> y

p_qualName :: ModuleName -> OccName -> R ()
p_qualName mname occName = do
  atom mname
  txt "."
  atom occName

-- | A helper for formatting infix constructions on the left-hand side of
-- definitions.
p_infixDefHelper ::
  -- | Whether to format in infix style
  Choice "infixStyle" ->
  -- | Whether to bump indentation for arguments
  Choice "indentArgs" ->
  -- | How to print the operator\/name
  R () ->
  -- | How to print the arguments
  [R ()] ->
  R ()
p_infixDefHelper isInfix indentArgs name args =
  case (Choice.toBool isInfix, args) of
    (True, p0 : p1 : ps) -> do
      let parens' =
            if null ps
              then id
              else parens N
      parens' $ do
        p0
        breakpoint
        inci . sitcc $ do
          name
          space
          p1
      unless (null ps) . inciIf (Choice.toBool indentArgs) $ do
        breakpoint
        sitcc (sep breakpoint sitcc ps)
    (_, ps) -> do
      name
      unless (null ps) $ do
        breakpoint
        inciIf (Choice.toBool indentArgs) $
          sitcc (sep breakpoint sitcc args)

-- | Print a Haddock.
--
-- The author's own text is reused whenever it can be, so a @{- | … -}@
-- comes back as a block comment and an empty @-- |@ survives; see
-- 'haddockAsWritten' for when it cannot be. Otherwise the Haddock is
-- rebuilt from its 'HsDocString'.
p_hsDoc ::
  -- | Haddock style
  HaddockStyle ->
  -- | Finish the doc string with a newline
  Choice "endNewline" ->
  -- | The 'LHsDoc' to render
  LHsDoc GhcPs ->
  R ()
p_hsDoc hstyle needsNewline = p_hsDocWith hstyle needsNewline (Isn't #mayShareLine)

-- | 'p_hsDoc' for a Haddock inside a construct that may legitimately be laid
-- out on one line.
--
-- A Haddock that comes back out as @{- | … -}@ is self-delimiting, so it
-- ends with a 'breakpoint' rather than a newline and can share the line:
-- @data A = A {- | a number -} Int Bool@ stays as written. One rendered as
-- @--@ lines still ends the line, since it owns the rest of it.
p_hsDocInline :: HaddockStyle -> LHsDoc GhcPs -> R ()
p_hsDocInline hstyle = p_hsDocWith hstyle (With #endNewline) (Is #mayShareLine)

-- | The worker behind 'p_hsDoc' and 'p_hsDocInline'.
p_hsDocWith ::
  HaddockStyle ->
  Choice "endNewline" ->
  Choice "mayShareLine" ->
  LHsDoc GhcPs ->
  R ()
p_hsDocWith hstyle needsNewline mayShareLine (L l str) = do
  let goesAfterCommentOrHaddock = \case
        LastEmittedHaddock _ -> True
        LastEmittedComment _ -> True
        _ -> False
  goesAfterComment <- goesAfterCommentOrHaddock <$> getLastEmitted
  -- Make sure the Haddock is separated by a newline from other comments.
  when goesAfterComment newline
  -- Print what the author wrote when we still have it. Rebuilding the
  -- comment from the doc string cannot preserve a @{- | … -}@ or an empty
  -- @-- |@, and what it loses it loses from the AST too.
  asWritten <- haddockAsWritten hstyle (L l str)
  case asWritten of
    Just written -> do
      let lns = unComment written
      sitcc . sequence_ . NE.intersperse newline . fmap txt $ lns
    Nothing -> do
      let docStringLines = splitDocString $ hsDocString str
          docPrefix = case hstyle of
            Pipe -> "-- |"
            Caret -> "-- ^"
            Asterisk n -> "-- " <> T.replicate n "*"
            Named name -> hsDocNameText name
      forM_ (zip docStringLines (True : repeat False)) $ \(x, isFirst) -> do
        if isFirst
          then txt docPrefix
          else newline >> txt "--"
        space
        unless (T.null x) (txt x)
  -- A Haddock rendered as @--@ lines owns the rest of its line and has to
  -- end it. One rendered as @{- | … -}@ is self-delimiting, so a space will
  -- do when the surrounding layout is single-line.
  when (Choice.isTrue needsNewline) $
    if Choice.isTrue mayShareLine && maybe False isMultilineComment asWritten
      then breakpoint
      else newline
  case l of
    UnhelpfulSpan _ ->
      -- It's often the case that the comment itself doesn't have a span
      -- attached to it, and instead its location can be obtained from the
      -- nearest enclosing span.
      getEnclosingSpan >>= mapM_ (setLastEmitted . LastEmittedHaddock)
    RealSrcSpan spn _ -> setLastEmitted (LastEmittedHaddock spn)

-- | Lay the computation out on several lines if rendering the given
-- fragment of the syntax tree will emit a Haddock as @--@ lines.
--
-- Such a Haddock takes whole lines: emitted inside a bracketed construct
-- that was put on one line, it swallows the rest of that line, closing
-- bracket and all. The author writes it in front of the construct, so its
-- span is outside the construct's and 'switchLayout' cannot see it; what
-- decides is where it will be /printed/, which is inside. Hence
-- @data A = A deriving (Eq)@ documented on the @Eq@ came out as
-- @deriving (-- \| B@, and a documented field of a one-line record as
-- @{-- \| …@, which does not parse at all.
--
-- A Haddock that comes back out as @{- | … -}@ is self-delimiting and does
-- not force anything, so @data A = A {- | a number -} Int Bool@ is left
-- alone rather than being exploded over five lines.
multiLineIfDocumented :: (Data a) => a -> R () -> R ()
multiLineIfDocumented x m = do
  breaks <- hasLineHaddocks x
  if breaks then enterLayout MultiLine m else m

-- | 'switchLayout', except that the layout is multi-line regardless of the
-- spans when rendering the given fragment will emit a Haddock as @--@
-- lines.
--
-- Use this rather than 'multiLineIfDocumented' around a 'switchLayout': the
-- override has to be applied after the spans have had their say, or it is
-- immediately discarded.
switchLayoutDocumented ::
  (Data a) =>
  -- | Fragment that decides whether documentation will be printed
  a ->
  -- | Span that controls layout otherwise
  [SrcSpan] ->
  -- | Computation to run with changed layout
  R () ->
  R ()
switchLayoutDocumented x spans' =
  switchLayout spans' . multiLineIfDocumented x

-- | Does rendering this fragment emit a Haddock as @--@ lines?
--
-- Every site this is asked about prints its Haddocks in 'Pipe' style, which
-- is what decides whether the author's own text can be reused.
hasLineHaddocks :: (Data a) => a -> R Bool
hasLineHaddocks x = case listify (const True :: LHsDoc GhcPs -> Bool) x of
  -- A doc string that is not reachable as an 'LHsDoc' cannot be inspected,
  -- so assume the worst and break.
  [] -> pure (containsHaddocks x)
  docs -> or <$> traverse rendersAsLines docs
  where
    rendersAsLines doc =
      maybe True (not . isMultilineComment) <$> haddockAsWritten Pipe doc

-- | The author's own text for a Haddock, when it can be reused.
--
-- 'Nothing' means the Haddock has to be rebuilt from its 'HsDocString' as
-- @--@ lines: either its text was not kept, or it is about to be rendered
-- in a different style than it was written in. Ormolu moves a trailing
-- @-- ^ X@ in front of what it documents and writes it as @-- | X@, and
-- keeping the author's text there would leave a @^@ pointing at the wrong
-- thing.
haddockAsWritten :: HaddockStyle -> LHsDoc GhcPs -> R (Maybe Comment)
haddockAsWritten hstyle (L l _) = do
  asWritten <- maybe (pure Nothing) lookupHaddockText (srcSpanToRealSrcSpan l)
  pure (mfilter (writtenAs hstyle . NE.head . unComment) asWritten)

-- | Was the Haddock written in the style it is about to be rendered in?
writtenAs :: HaddockStyle -> Text -> Bool
writtenAs hstyle firstLine =
  case T.stripPrefix "--" opener of
    Just rest -> hasTrigger (T.stripStart rest)
    Nothing -> maybe False (hasTrigger . T.stripStart) (T.stripPrefix "{-" opener)
  where
    opener = T.stripStart firstLine
    hasTrigger t = case hstyle of
      Pipe -> "|" `T.isPrefixOf` t
      Caret -> "^" `T.isPrefixOf` t
      Asterisk n ->
        T.replicate n "*" `T.isPrefixOf` t
          && not (T.replicate (n + 1) "*" `T.isPrefixOf` t)
      Named name -> ("$" <> T.pack name) `T.isPrefixOf` t

-- | Print the anchor of a named doc section. Unlike 'p_hsDoc' this is a
-- bare anchor with no doc string attached, so there is no span to report.
p_hsDocName :: String -> R ()
p_hsDocName name = do
  txt (hsDocNameText name)

-- | Render the anchor of a named doc section.
hsDocNameText :: String -> Text
hsDocNameText name = "-- $" <> T.pack name

p_sourceText :: SourceText -> R ()
p_sourceText = \case
  NoSourceText -> pure ()
  SourceText s -> atom @FastString s

p_namespaceSpec :: NamespaceSpecifier -> R ()
p_namespaceSpec = \case
  NoNamespaceSpecifier -> pure ()
  TypeNamespaceSpecifier _ -> txt "type" *> space
  DataNamespaceSpecifier _ -> txt "data" *> space

p_hsMultAnn :: (mult -> R ()) -> HsMultAnnOf mult GhcPs -> R ()
p_hsMultAnn p_mult = \case
  HsUnannotated _ -> pure ()
  HsLinearAnn _ -> txt "%1"
  HsExplicitMult _ mult -> txt "%" *> p_mult mult