packages feed

cabal-gild-1.8.4.0: source/library/CabalGild/Unstable/Action/EvaluatePragmas/Fragment.hs

{-# LANGUAGE ScopedTypeVariables #-}

module CabalGild.Unstable.Action.EvaluatePragmas.Fragment where

import qualified CabalGild.Unstable.Class.MonadRead as MonadRead
import qualified CabalGild.Unstable.Class.MonadWarn as MonadWarn
import qualified CabalGild.Unstable.Extra.Name as Name
import qualified CabalGild.Unstable.Extra.SectionArg as SectionArg
import qualified CabalGild.Unstable.Extra.String as String
import qualified CabalGild.Unstable.Type.Comment as Comment
import qualified CabalGild.Unstable.Type.Comments as Comments
import qualified CabalGild.Unstable.Type.Input as Input
import qualified CabalGild.Unstable.Type.Pragma as Pragma
import qualified Control.Exception as IO
import qualified Control.Monad as Monad
import qualified Control.Monad.Catch as Exception
import qualified Distribution.Compat.CharParsing as CharParsing
import qualified Distribution.FieldGrammar.Newtypes as Newtypes
import qualified Distribution.Fields as Fields
import qualified Distribution.Parsec as Parsec
import qualified Distribution.Utils.Generic as Utils
import qualified System.FilePath as FilePath

-- | High level wrapper that traverses all fields and evaluates fragment pragmas.
run ::
  (Exception.MonadCatch m, MonadRead.MonadRead m, MonadWarn.MonadWarn m) =>
  FilePath ->
  ([Fields.Field (p, Comments.Comments q)], [Comment.Comment q]) ->
  m ([Fields.Field (p, Comments.Comments q)], [Comment.Comment q])
run p (fs, cs) = (,) <$> traverse (field p) fs <*> pure cs

-- | Evaluates fragment pragmas on a single field or section. For sections,
-- recurses into child fields.
field ::
  (Exception.MonadCatch m, MonadRead.MonadRead m, MonadWarn.MonadWarn m) =>
  FilePath ->
  Fields.Field (p, Comments.Comments q) ->
  m (Fields.Field (p, Comments.Comments q))
field p f = case f of
  Fields.Field n _ -> do
    result <- tryFragment p n
    case result of
      Nothing -> pure f
      Just fragmentFields -> case fragmentFields of
        [] -> do
          MonadWarn.warnLn "warning: fragment file is empty"
          pure f
        Fields.Section {} : _ -> do
          MonadWarn.warnLn
            "warning: fragment contains a section, but the pragma is on a field"
          pure f
        Fields.Field (Fields.Name _ n') fls' : _ -> do
          if Name.value n == n'
            then
              let position = fst $ Name.annotation n
               in pure . Fields.Field n $
                    fmap ((position, Comments.empty) <$) fls'
            else do
              MonadWarn.warnLn $
                "warning: fragment contains field \""
                  <> String.fromUtf8 n'
                  <> "\", but expected \""
                  <> String.fromUtf8 (Name.value n)
                  <> "\""
              pure f
  Fields.Section n sas children -> do
    result <- tryFragment p n
    case result of
      Nothing -> Fields.Section n sas <$> traverse (field p) children
      Just fragmentFields -> case fragmentFields of
        [] -> do
          MonadWarn.warnLn "warning: fragment file is empty"
          Fields.Section n sas <$> traverse (field p) children
        Fields.Field (Fields.Name _ n') _ : _ -> do
          MonadWarn.warnLn $
            "warning: fragment contains a field \""
              <> String.fromUtf8 n'
              <> "\", but the pragma is on a section"
          Fields.Section n sas <$> traverse (field p) children
        Fields.Section (Fields.Name _ n') sas' fs' : _ -> do
          if Name.value n == n' && fmap SectionArg.value sas == fmap SectionArg.value sas'
            then
              let position = fst $ Name.annotation n
               in pure . Fields.Section n sas $
                    fmap (fmap (const (position, Comments.empty))) fs'
            else do
              MonadWarn.warnLn $
                "warning: fragment contains section \""
                  <> showSection n' sas'
                  <> "\", but expected \""
                  <> showSection (Name.value n) sas
                  <> "\""
              Fields.Section n sas <$> traverse (field p) children

-- | Tries to find and read a fragment pragma from the last "before" comment on
-- a field/section name. Returns 'Nothing' if no fragment pragma is present.
-- Returns 'Just fields' with the parsed fragment content on success, or
-- 'Nothing' on read/parse failure (with a warning emitted).
tryFragment ::
  (Exception.MonadCatch m, MonadRead.MonadRead m, MonadWarn.MonadWarn m) =>
  FilePath ->
  Fields.Name (p, Comments.Comments q) ->
  m (Maybe [Fields.Field Parsec.Position])
tryFragment p n = do
  let comments = Comments.toList . snd $ Name.annotation n
  case Utils.safeLast comments of
    Nothing -> pure Nothing
    Just comment -> case Parsec.simpleParsecBS $ Comment.value comment of
      Nothing -> pure Nothing
      Just (Pragma.Pragma (Fragment token)) -> do
        let root = FilePath.takeDirectory p
            path = FilePath.normalise $ FilePath.combine root token
        result <- Exception.try $ MonadRead.read (Input.File path)
        case result of
          Left (_ :: IO.IOException) -> do
            MonadWarn.warnLn $
              "warning: could not read fragment " <> show path
            pure Nothing
          Right contents -> case Fields.readFields contents of
            Left _ -> do
              MonadWarn.warnLn $
                "warning: could not parse fragment " <> show path
              pure Nothing
            Right fields -> pure $ Just fields

-- | Renders a section name and its arguments as a human-readable string for
-- use in warning messages.
showSection :: Fields.FieldName -> [Fields.SectionArg a] -> String
showSection n [] = String.fromUtf8 n
showSection n args =
  String.fromUtf8 n
    <> " "
    <> unwords (fmap (String.fromUtf8 . SectionArg.value) args)

-- | The fragment pragma type. Parsed from @-- cabal-gild: fragment FILE@.
newtype Fragment = Fragment String
  deriving (Eq, Show)

instance Parsec.Parsec Fragment where
  parsec = do
    Monad.void $ CharParsing.string "fragment"
    CharParsing.skipSpaces1
    Fragment . Newtypes.getToken' <$> Parsec.parsec