packages feed

tilia-0.0.1.0: src/Tilia/Render/Declaration.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

-- | Declarations: dispatching to the right printer, and grouping.
--
-- Two jobs live here. The first is a case over every kind of declaration,
-- which is mostly a matter of handing the work on; the few forms with no
-- module of their own—foreign imports, annotations, @default@ declarations,
-- top-level splices—are printed here rather than in four files of a dozen
-- lines each.
--
-- The second is grouping, which is the interesting one. A blank line between
-- declarations is meaningful to a reader, so a signature and the function it
-- describes should stay together while unrelated declarations are kept
-- apart. Nothing in the syntax tree says which declarations belong together,
-- so it is worked out from what they are and what they name.
module Tilia.Render.Declaration
  ( decls,
    declsKeepingGroups,
  )
where

import Data.List (sort)
import Data.List.NonEmpty (NonEmpty (..), (<|))
import Data.List.NonEmpty qualified as NE
import GHC.Data.FastString (unpackFS)
import GHC.Hs
import GHC.Types.ForeignCall (CExportSpec (..))
import GHC.Types.Name.Occurrence (occNameFS)
import GHC.Types.Name.Reader (RdrName, rdrNameOcc)
import GHC.Types.SourceText
import GHC.Types.SrcLoc (GenLocated (..), isGoodSrcSpan, unLoc)
import Tilia.Doc.Combinators
import Tilia.Render.Class
import Tilia.Render.Context
import Tilia.Render.Data
import Tilia.Render.Expression
import Tilia.Render.Haddock
import Tilia.Render.Layout
import Tilia.Render.Literal (stringLiteral)
import Tilia.Render.Name
import Tilia.Render.Pragma
import Tilia.Render.Signature
import Tilia.Render.Type
import Tilia.Span
import Tilia.Span.Ghc

----------------------------------------------------------------------------
-- Runs of declarations

-- | A run of declarations, with blank lines wherever we think they belong.
decls :: Ctx -> FamilyStyle -> [LHsDecl GhcPs] -> Doc
decls = declRun Disregard

-- | A run of declarations that keeps the author's grouping.
--
-- Where the author ran declarations together we run them together too, and
-- where they left a blank line we leave one. The exception is documentation:
-- a documented declaration always gets air around it, since a Haddock that
-- butts up against the declaration above reads as belonging to that one.
declsKeepingGroups :: Ctx -> FamilyStyle -> [LHsDecl GhcPs] -> Doc
declsKeepingGroups = declRun Respect

-- | Whether the author's own blank lines are consulted.
data Grouping
  = Disregard
  | Respect
  deriving (Eq, Show)

declRun :: Grouping -> Ctx -> FamilyStyle -> [LHsDecl GhcPs] -> Doc
declRun grouping ctx style ds =
  items NoBrace $ case groups of
    [] -> []
    (firstGroup : rest) ->
      render firstGroup <> concat (zipWith withGap groups rest)
  where
    isSignatureFile = ctxSourceType ctx == SignatureSource
    groups = groupDecls ctx isSignatureFile ds
    render = NE.toList . fmap (at_ ctx (hsDecl ctx style))

    withGap previous current
      | separate previous current = breakOrSpace : render current
      | otherwise = render current

    separate previous current = case grouping of
      Disregard -> True
      Respect ->
        separatedByBlank ctx ended began
          || commentBetween ctx ended began
          || isDocumented previous
          || isDocumented current
      where
        ended = spanOf (NE.last previous)
        began = spanOf (NE.head current)

    isDocumented = any (isDocNext . unLoc)
    isDocNext = \case
      DocD _ (DocCommentNext _) -> True
      DocD _ (DocCommentPrev _) -> True
      _ -> False

-- | Gather declarations that belong together.
groupDecls :: Ctx -> Bool -> [LHsDecl GhcPs] -> [NonEmpty (LHsDecl GhcPs)]
groupDecls _ _ [] = []
groupDecls ctx isSignatureFile (d : ds)
  -- A Haddock documenting what follows belongs to the group that follows,
  -- not to a group of its own—unless what follows is another Haddock, which
  -- documents nothing either. Those two have to be kept apart: run
  -- together they are not two doc comments but one.
  | isDocNext (unLoc d) = case groupDecls ctx isSignatureFile ds of
      [] -> [d :| []]
      (g : gs)
        | isDoc (unLoc (NE.head g)) -> (d :| []) : g : gs
        | otherwise -> (d <| g) : gs
  | otherwise =
      let (together, rest) = span belongs (zip (d : ds) ds)
       in (d :| map snd together) : groupDecls ctx isSignatureFile (map snd rest)
  where
    isDocNext = \case
      DocD _ (DocCommentNext _) -> True
      _ -> False
    isDoc = \case
      DocD _ _ -> True
      _ -> False
    belongs (previous, current) =
      (not isSignatureFile && isSignatureSeries ctx previous current)
        || isDerivingSeries ctx previous current
        || relatedDecls d current
        || relatedDecls previous current

-- | A run of type signatures with nothing between them is a list, and a
-- list reads better without gaps in it.
isSignatureSeries :: Ctx -> LHsDecl GhcPs -> LHsDecl GhcPs -> Bool
isSignatureSeries ctx x@(L _ a) y@(L _ b) = case (a, b) of
  (SigD _ TypeSig {}, SigD _ TypeSig {}) ->
    not (commentBetween ctx (spanOf x) (spanOf y))
  _ -> False

-- | Two standalone @deriving@ declarations the author ran together.
isDerivingSeries :: Ctx -> LHsDecl GhcPs -> LHsDecl GhcPs -> Bool
isDerivingSeries ctx x@(L _ a) y@(L _ b) = case (a, b) of
  (DerivD {}, DerivD {}) ->
    not (separatedByBlank ctx (spanOf x) (spanOf y))
  _ -> False

----------------------------------------------------------------------------
-- What a declaration is about

-- | The kinds of declaration that grouping distinguishes.
--
-- Anything not named here groups with nothing, which is the right default:
-- an unrecognised declaration standing on its own is merely a missed
-- opportunity, whereas one wrongly attached to its neighbour is a mistake.
data Kind
  = TypeSignature
  | DefaultSignature
  | FunctionBody
  | PatternSignature
  | PatternDefinition
  | DataDeclaration
  | ClassDeclaration
  | KindSignature
  | FamilyDeclaration
  | TypeSynonym
  | PragmaDeclaration
  | TopLevelSplice
  | DocumentsNext
  | DocumentsPrevious
  | Unremarkable
  deriving (Eq, Show)

-- | What a declaration is, and what it names.
declKind :: HsDecl GhcPs -> (Kind, [RdrName])
declKind = \case
  SigD _ (TypeSig _ ns _) -> (TypeSignature, map unLoc ns)
  SigD _ (ClassOpSig _ True ns _) -> (DefaultSignature, map unLoc ns)
  SigD _ (ClassOpSig _ False ns _) -> (TypeSignature, map unLoc ns)
  SigD _ (PatSynSig _ ns _) -> (PatternSignature, map unLoc ns)
  SigD _ (InlineSig _ (L _ n) _) -> (PragmaDeclaration, [n])
  SigD _ (SCCFunSig _ (L _ n) _) -> (PragmaDeclaration, [n])
  SigD _ sig
    | Just n <- specialisedName sig -> (PragmaDeclaration, [n])
  ValD _ (FunBind _ (L _ n) _) -> (FunctionBody, [n])
  ValD _ (PatBind _ p _ _) -> (FunctionBody, boundNames p)
  ValD _ (PatSynBind _ (PSB _ (L _ n) _ _ _)) -> (PatternDefinition, [n])
  AnnD _ (HsAnnotation _ (ValueAnnProvenance (L _ n)) _) -> (PragmaDeclaration, [n])
  AnnD _ (HsAnnotation _ (TypeAnnProvenance (L _ n)) _) -> (PragmaDeclaration, [n])
  WarningD _ (Warnings _ ws) ->
    (PragmaDeclaration, [unLoc n | L _ (Warning _ ns _) <- ws, n <- ns])
  TyClD _ (DataDecl _ (L _ n) _ _ _) -> (DataDeclaration, [n])
  TyClD _ (ClassDecl {tcdLName = L _ n}) -> (ClassDeclaration, [n])
  TyClD _ (SynDecl _ (L _ n) _ _ _) -> (TypeSynonym, [n])
  TyClD _ (FamDecl _ (FamilyDecl _ _ _ (L _ n) _ _ _ _)) -> (FamilyDeclaration, [n])
  KindSigD _ (StandaloneKindSig _ (L _ n) _) -> (KindSignature, [n])
  SpliceD _ (SpliceDecl _ _ _) -> (TopLevelSplice, [])
  DocD _ (DocCommentNext _) -> (DocumentsNext, [])
  DocD _ (DocCommentPrev _) -> (DocumentsPrevious, [])
  _ -> (Unremarkable, [])

-- | The names a pattern binding brings into scope.
boundNames :: LPat GhcPs -> [RdrName]
boundNames (L _ p) = case p of
  VarPat _ (L _ n) -> [n]
  AsPat _ (L _ n) inner -> n : boundNames inner
  NPlusKPat _ (L _ n) _ _ _ _ -> [n]
  LazyPat _ inner -> boundNames inner
  BangPat _ inner -> boundNames inner
  ParPat _ inner -> boundNames inner
  SigPat _ inner _ -> boundNames inner
  ViewPat _ _ inner -> boundNames inner
  SumPat _ inner _ _ -> boundNames inner
  TuplePat _ ps _ -> concatMap boundNames ps
  ListPat _ ps -> concatMap boundNames ps
  OrPat _ ps -> concatMap boundNames (NE.toList ps)
  ConPat _ _ details -> concatMap boundNames (hsConPatArgs details)
  _ -> []

-- | Should these two declarations be printed with no blank line between
-- them?
relatedDecls :: LHsDecl GhcPs -> LHsDecl GhcPs -> Bool
relatedDecls a b = case (kindA, kindB) of
  (DocumentsNext, _) -> True
  (_, DocumentsPrevious) -> True
  -- Splices name nothing, so the only evidence they belong together is that
  -- the author wrote them together.
  (TopLevelSplice, TopLevelSplice) -> not (blankBetween (spanOf a) (spanOf b))
  pair | pair `elem` relatedKinds -> shareAName namesA namesB
  _ -> False
  where
    (kindA, namesA) = declKind (unLoc a)
    (kindB, namesB) = declKind (unLoc b)

-- | The pairs of declaration kinds that group when they name something in
-- common.
--
-- Reading this as a list rather than as nested cases is the point: what
-- belongs with what is a policy, and a policy is easier to check when it is
-- written out.
relatedKinds :: [(Kind, Kind)]
relatedKinds =
  [ (TypeSignature, FunctionBody),
    (TypeSignature, DefaultSignature),
    (DefaultSignature, TypeSignature),
    (DefaultSignature, FunctionBody),
    (TypeSignature, PragmaDeclaration),
    (PragmaDeclaration, TypeSignature),
    (PragmaDeclaration, FunctionBody),
    (FunctionBody, PragmaDeclaration),
    (PragmaDeclaration, DataDeclaration),
    (DataDeclaration, PragmaDeclaration),
    (PragmaDeclaration, PragmaDeclaration),
    (PatternSignature, PatternDefinition),
    (KindSignature, DataDeclaration),
    (KindSignature, ClassDeclaration),
    (KindSignature, FamilyDeclaration),
    (KindSignature, TypeSynonym)
  ]

-- | Do the two declarations name anything in common?
--
-- Names are compared as text rather than as parsed names, since a pragma
-- may name a constructor where the declaration names the type, and the two
-- are different parsed names for the same spelling.
shareAName :: [RdrName] -> [RdrName] -> Bool
shareAName xs ys = overlaps (sort (map spelling xs)) (sort (map spelling ys))
  where
    spelling :: RdrName -> String
    spelling = unpackFS . occNameFS . rdrNameOcc
    overlaps (a : as) (b : bs)
      | a < b = overlaps as (b : bs)
      | a > b = overlaps (a : as) bs
      | otherwise = True
    overlaps _ _ = False

----------------------------------------------------------------------------
-- One declaration

-- | Print one declaration.
hsDecl :: Ctx -> FamilyStyle -> HsDecl GhcPs -> Doc
hsDecl ctx style = \case
  TyClD _ x -> tyClDecl ctx style x
  ValD _ x -> valDecl ctx NoBrace x
  SigD _ x -> sigDecl ctx x
  InstD _ x -> instDecl ctx style x
  DerivD _ x -> standaloneDerivDecl ctx x
  DefD _ x -> defaultDecl ctx x
  ForD _ x -> foreignDecl ctx x
  WarningD _ x -> warnDecls ctx x
  AnnD _ x -> annDecl ctx x
  RuleD _ x -> ruleDecls ctx x
  SpliceD _ (SpliceDecl NoExtField splice deco) ->
    at ctx splice (untypedSplice ctx deco)
  RoleAnnotD _ x -> roleAnnot ctx x
  KindSigD _ x -> standaloneKindSig ctx x
  DocD _ x -> case x of
    DocCommentNext str -> haddock ctx Pipe Open str
    DocCommentPrev str -> haddock ctx Caret Open str
    DocCommentNamed n str -> haddock ctx (Chunk n) Open str
    DocGroup n str -> haddock ctx (Section n) Open str

tyClDecl :: Ctx -> FamilyStyle -> TyClDecl GhcPs -> Doc
tyClDecl ctx style = \case
  FamDecl _ x -> famDecl ctx style x
  SynDecl {..} -> synDecl ctx tcdLName tcdFixity tcdTyVars tcdRhs
  DataDecl {..} ->
    dataDecl
      ctx
      Associated
      tcdLName
      (hsq_explicit tcdTyVars)
      spanOf
      (at_ ctx (tyVarBndr ctx))
      tcdFixity
      mempty
      tcdDataDefn
  ClassDecl {tcdCExt = (anns, _, _), ..} ->
    classDecl
      ctx
      anns
      tcdCtxt
      tcdLName
      tcdTyVars
      tcdFixity
      tcdFDs
      tcdSigs
      tcdMeths
      tcdATs
      tcdATDefs
      tcdDocs

instDecl :: Ctx -> FamilyStyle -> InstDecl GhcPs -> Doc
instDecl ctx style = \case
  ClsInstD _ x -> clsInstDecl ctx x
  TyFamInstD _ x -> tyFamInstDecl ctx style x
  DataFamInstD _ x -> dataFamInstDecl ctx style x

----------------------------------------------------------------------------
-- The declarations with nowhere else to live

-- | A @default@ declaration.
defaultDecl :: Ctx -> DefaultDecl GhcPs -> Doc
defaultDecl ctx (DefaultDecl _ className types) =
  txt "default"
    <> foldMap (\c -> breakOrSpace <> name ctx c) className
    <> breakOrSpace
    <> indent (parens (commaSep (map (align . hsType ctx) types)))

-- | An @ANN@ pragma.
annDecl :: Ctx -> AnnDecl GhcPs -> Doc
annDecl ctx (HsAnnotation _ provenance e) =
  pragma "ANN" . indent $
    subject <> breakOrSpace <> hsExpr ctx e
  where
    subject = case provenance of
      ValueAnnProvenance n -> name ctx n
      TypeAnnProvenance n -> txt "type" <> space <> name ctx n
      ModuleAnnProvenance -> txt "module"

-- | A foreign import or export.
foreignDecl :: Ctx -> ForeignDecl GhcPs -> Doc
foreignDecl ctx = \case
  fd@ForeignImport {fd_fi} -> foreignImport ctx fd_fi <> foreignSig ctx fd
  fd@ForeignExport {fd_fe} -> foreignExport ctx fd_fe <> foreignSig ctx fd

-- | The name and type that end a foreign declaration.
foreignSig :: Ctx -> ForeignDecl GhcPs -> Doc
foreignSig ctx fd =
  breakOrSpace
    <> indent
      ( layoutFrom ctx (spanOf (fd_name fd) <> spanOf (fd_sig_ty fd)) $
          name ctx (fd_name fd) <> typeAscription ctx (fd_sig_ty fd)
      )

-- | The head of a foreign import.
foreignImport :: Ctx -> ForeignImport GhcPs -> Doc
foreignImport ctx (CImport src callConv safety _ _) =
  txt "foreign import"
    <> space
    <> at ctx callConv outputable
    <> includeWhen (isGoodSrcSpan (getLocA safety)) (space <> outputable safety)
    <> indent
      ( at ctx src $ \case
          NoSourceText -> mempty
          SourceText lit -> breakOrSpace <> stringLiteral lit
      )

foreignExport :: Ctx -> ForeignExport GhcPs -> Doc
foreignExport ctx (CExport src (L loc (CExportStatic _ _ callConv))) =
  txt "foreign export"
    <> space
    <> at ctx (L loc callConv) outputable
    <> space
    <> at ctx src sourceText