packages feed

cabal-install-3.16.0.0: src/Distribution/Client/ParseUtils.hs

{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}

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

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

-- |
-- Module      :  Distribution.Client.ParseUtils
-- Maintainer  :  cabal-devel@haskell.org
-- Portability :  portable
--
-- Parsing utilities.
module Distribution.Client.ParseUtils
  ( -- * Fields and field utilities
    FieldDescr (..)
  , liftField
  , liftFields
  , addFields
  , aliasField
  , filterFields
  , mapFieldNames
  , commandOptionToField
  , commandOptionsToFields

    -- * Sections and utilities
  , SectionDescr (..)
  , liftSection

    -- * FieldGrammar sections
  , FGSectionDescr (..)

    -- * Parsing and printing flat config
  , parseFields
  , ppFields
  , ppSection

    -- * Parsing and printing config with sections and subsections
  , parseFieldsAndSections
  , ppFieldsAndSections

    -- ** Top level of config files
  , parseConfig
  , showConfig
  )
where

import Distribution.Client.Compat.Prelude hiding (empty, get)
import Prelude ()

import Distribution.Deprecated.ParseUtils
  ( Field (..)
  , FieldDescr (..)
  , LineNo
  , PError (..)
  , ParseResult (..)
  , liftField
  , lineNo
  , readFields
  , warning
  )
import Distribution.Deprecated.ViewAsFieldDescr
  ( viewAsFieldDescr
  )

import Distribution.Simple.Command
  ( OptionField
  )

import qualified Data.ByteString as BS
import qualified Data.Map as Map
import Text.PrettyPrint (($+$))
import qualified Text.PrettyPrint as Disp
  ( Doc
  , colon
  , empty
  , isEmpty
  , nest
  , text
  , vcat
  , (<>)
  )

-- For new parser stuff
import Distribution.CabalSpecVersion (cabalSpecLatest)
import Distribution.FieldGrammar (parseFieldGrammar, partitionFields)
import qualified Distribution.FieldGrammar as FG
import qualified Distribution.Fields as F
import Distribution.Fields.ParseResult (runParseResult)
import Distribution.Parsec.Error (showPError)
import Distribution.Parsec.Position (Position (..))
import Distribution.Parsec.Warning (showPWarning)
import Distribution.Simple.Utils (fromUTF8BS, toUTF8BS)

-------------------------
-- FieldDescr utilities
--

liftFields
  :: (b -> a)
  -> (a -> b -> b)
  -> [FieldDescr a]
  -> [FieldDescr b]
liftFields get set = map (liftField get set)

-- | Given a collection of field descriptions, keep only a given list of them,
-- identified by name.
--
-- TODO: This makes it easy to footgun by providing a non-existent field name.
filterFields :: [String] -> [FieldDescr a] -> [FieldDescr a]
filterFields includeFields = filter ((`elem` includeFields) . fieldName)

-- | Given a collection of field descriptions, get a field with a given name.
getField :: String -> [FieldDescr a] -> Maybe (FieldDescr a)
getField name = find ((== name) . fieldName)

-- | Apply a name mangling function to the field names of all the field
-- descriptions. The typical use case is to apply some prefix.
mapFieldNames :: (String -> String) -> [FieldDescr a] -> [FieldDescr a]
mapFieldNames mangleName =
  map (\descr -> descr{fieldName = mangleName (fieldName descr)})

-- | Reuse a command line 'OptionField' as a config file 'FieldDescr'.
commandOptionToField :: OptionField a -> FieldDescr a
commandOptionToField = viewAsFieldDescr

-- | Reuse a bunch of command line 'OptionField's as config file 'FieldDescr's.
commandOptionsToFields :: [OptionField a] -> [FieldDescr a]
commandOptionsToFields = map viewAsFieldDescr

-- | Add fields to a field list.
addFields
  :: [FieldDescr a]
  -> ([FieldDescr a] -> [FieldDescr a])
addFields = (++)

-- | Add a new field which is identical to an existing field but with a
-- different name.
aliasField
  :: String
  -- ^ The existing field name.
  -> String
  -- ^ The new field name.
  -> [FieldDescr a]
  -> [FieldDescr a]
aliasField oldName newName fields =
  let fieldToRename = getField oldName fields
   in case fieldToRename of
        -- TODO: Should this throw?
        Nothing -> fields
        Just fieldToRename' ->
          let newField = fieldToRename'{fieldName = newName}
           in newField : fields

------------------------------------------
-- SectionDescr definition and utilities
--

-- | The description of a section in a config file. It can contain both
-- fields and optionally further subsections. See also 'FieldDescr'.
data SectionDescr a = forall b.
  SectionDescr
  { sectionName :: String
  , sectionFields :: [FieldDescr b]
  , sectionSubsections :: [SectionDescr b]
  , sectionGet :: a -> [(String, b)]
  , sectionSet :: LineNo -> String -> b -> a -> ParseResult a
  , sectionEmpty :: b
  }

-- | 'FieldGrammar' section description
data FGSectionDescr g a = forall s.
  FGSectionDescr
  { fgSectionName :: String
  , fgSectionGrammar :: g s s
  , -- todo: add subsections?
    fgSectionGet :: a -> [(String, s)]
  , fgSectionSet :: LineNo -> String -> s -> a -> ParseResult a
  }

-- | To help construction of config file descriptions in a modular way it is
-- useful to define fields and sections on local types and then hoist them
-- into the parent types when combining them in bigger descriptions.
--
-- This is essentially a lens operation for 'SectionDescr' to help embedding
-- one inside another.
liftSection
  :: (b -> a)
  -> (a -> b -> b)
  -> SectionDescr a
  -> SectionDescr b
liftSection get' set' (SectionDescr name fields sections get set empty) =
  let sectionGet' = get . get'
      sectionSet' lineno param x y = do
        x' <- set lineno param x (get' y)
        return (set' x' y)
   in SectionDescr name fields sections sectionGet' sectionSet' empty

-------------------------------------
-- Parsing and printing flat config
--

-- | Parse a bunch of semi-parsed 'Field's according to a set of field
-- descriptions. It accumulates the result on top of a given initial value.
--
-- This only covers the case of flat configuration without subsections. See
-- also 'parseFieldsAndSections'.
parseFields :: [FieldDescr a] -> a -> [Field] -> ParseResult a
parseFields fieldDescrs =
  foldM setField
  where
    fieldMap = Map.fromList [(fieldName f, f) | f <- fieldDescrs]

    setField accum (F line name value) =
      case Map.lookup name fieldMap of
        Just (FieldDescr _ _ set) -> set line value accum
        Nothing -> do
          -- the 'world-file' field was removed in 3.8, however
          -- it was automatically added to many config files
          -- before that, so its warning is silently ignored
          unless (name == "world-file") $
            warning $
              "Unrecognized field " ++ name ++ " on line " ++ show line
          return accum
    setField accum f = do
      warning $ "Unrecognized stanza on line " ++ show (lineNo f)
      return accum

-- | This is a customised version of the functions from Distribution.Deprecated.ParseUtils
-- that also optionally print default values for empty fields as comments.
ppFields :: [FieldDescr a] -> (Maybe a) -> a -> Disp.Doc
ppFields fields def cur =
  Disp.vcat
    [ ppField name (fmap getter def) (getter cur)
    | FieldDescr name getter _ <- fields
    ]

ppField :: String -> (Maybe Disp.Doc) -> Disp.Doc -> Disp.Doc
ppField name mdef cur
  | Disp.isEmpty cur =
      maybe
        Disp.empty
        ( \def ->
            Disp.text "--"
              <+> Disp.text name
                Disp.<> Disp.colon
              <+> def
        )
        mdef
  | otherwise = Disp.text name Disp.<> Disp.colon <+> cur

-- | Pretty print a section.
--
-- Since 'ppFields' does not cover subsections you can use this to add them.
-- Or alternatively use a 'SectionDescr' and use 'ppFieldsAndSections'.
ppSection :: String -> String -> [FieldDescr a] -> (Maybe a) -> a -> Disp.Doc
ppSection name arg fields def cur
  | Disp.isEmpty fieldsDoc = Disp.empty
  | otherwise =
      Disp.text name
        <+> argDoc
        $+$ (Disp.nest 2 fieldsDoc)
  where
    fieldsDoc = ppFields fields def cur
    argDoc
      | arg == "" = Disp.empty
      | otherwise = Disp.text arg

-----------------------------------------
-- Parsing and printing non-flat config
--

-- | Much like 'parseFields' but it also allows subsections. The permitted
-- subsections are given by a list of 'SectionDescr's.
parseFieldsAndSections
  :: [FieldDescr a]
  -- ^ field
  -> [SectionDescr a]
  -- ^ legacy sections
  -> [FGSectionDescr FG.ParsecFieldGrammar a]
  -- ^ FieldGrammar sections
  -> a
  -> [Field]
  -> ParseResult a
parseFieldsAndSections fieldDescrs sectionDescrs fgSectionDescrs =
  foldM setField
  where
    fieldMap = Map.fromList [(fieldName f, f) | f <- fieldDescrs]
    sectionMap = Map.fromList [(sectionName s, s) | s <- sectionDescrs]
    fgSectionMap = Map.fromList [(fgSectionName s, s) | s <- fgSectionDescrs]

    setField a (F line name value) =
      case Map.lookup name fieldMap of
        Just (FieldDescr _ _ set) -> set line value a
        Nothing ->
          case Left <$> Map.lookup name sectionMap <|> Right <$> Map.lookup name fgSectionMap of
            Just _ -> ParseFailed $ FieldShouldBeStanza name line
            Nothing -> do
              warning $
                "Unrecognized field '"
                  ++ name
                  ++ "' on line "
                  ++ show line
              return a
    setField a (Section line name param fields) =
      case Left <$> Map.lookup name sectionMap <|> Right <$> Map.lookup name fgSectionMap of
        Just (Left (SectionDescr _ fieldDescrs' sectionDescrs' _ set sectionEmpty)) -> do
          b <- parseFieldsAndSections fieldDescrs' sectionDescrs' [] sectionEmpty fields
          set line param b a
        Just (Right (FGSectionDescr _ grammar _getter setter)) -> do
          let fields1 = map convertField fields
              (fields2, sections) = partitionFields fields1
          -- TODO: recurse into sections
          for_ (concat sections) $ \(FG.MkSection (F.Name (Position line' _) name') _ _) ->
            warning $
              "Unrecognized section '"
                ++ fromUTF8BS name'
                ++ "' on line "
                ++ show line'
          case runParseResult $ parseFieldGrammar cabalSpecLatest fields2 grammar of
            (warnings, Right b) -> do
              for_ warnings $ \w -> warning $ showPWarning "???" w
              setter line param b a
            (warnings, Left (_, errs)) -> do
              for_ warnings $ \w -> warning $ showPWarning "???" w
              case errs of
                err :| _errs -> fail $ showPError "???" err
        Nothing -> do
          warning $
            "Unrecognized section '"
              ++ name
              ++ "' on line "
              ++ show line
          return a

convertField :: Field -> F.Field Position
convertField (F line name str) =
  F.Field (F.Name pos (toUTF8BS name)) [F.FieldLine pos $ toUTF8BS str]
  where
    pos = Position line 0
-- arguments omitted
convertField (Section line name _arg fields) =
  F.Section (F.Name pos (toUTF8BS name)) [] (map convertField fields)
  where
    pos = Position line 0

-- | Much like 'ppFields' but also pretty prints any subsections. Subsection
-- are only shown if they are non-empty.
--
-- Note that unlike 'ppFields', at present it does not support printing
-- default values. If needed, adding such support would be quite reasonable.
ppFieldsAndSections :: [FieldDescr a] -> [SectionDescr a] -> [FGSectionDescr FG.PrettyFieldGrammar a] -> a -> Disp.Doc
ppFieldsAndSections fieldDescrs sectionDescrs fgSectionDescrs val =
  ppFields fieldDescrs Nothing val
    $+$ Disp.vcat
      ( [ Disp.text "" $+$ sectionDoc
        | SectionDescr
            { sectionName
            , sectionGet
            , sectionFields
            , sectionSubsections
            } <-
            sectionDescrs
        , (param, x) <- sectionGet val
        , let sectionDoc =
                ppSectionAndSubsections
                  sectionName
                  param
                  sectionFields
                  sectionSubsections
                  []
                  x
        , not (Disp.isEmpty sectionDoc)
        ]
          ++ [ Disp.text "" $+$ sectionDoc
             | FGSectionDescr{fgSectionName, fgSectionGrammar, fgSectionGet} <- fgSectionDescrs
             , (param, x) <- fgSectionGet val
             , let sectionDoc = ppFgSection fgSectionName param fgSectionGrammar x
             , not (Disp.isEmpty sectionDoc)
             ]
      )

-- | Unlike 'ppSection' which has to be called directly, this gets used via
-- 'ppFieldsAndSections' and so does not need to be exported.
ppSectionAndSubsections
  :: String
  -> String
  -> [FieldDescr a]
  -> [SectionDescr a]
  -> [FGSectionDescr FG.PrettyFieldGrammar a]
  -> a
  -> Disp.Doc
ppSectionAndSubsections name arg fields sections fgSections cur
  | Disp.isEmpty fieldsDoc = Disp.empty
  | otherwise =
      Disp.text name
        <+> argDoc
        $+$ (Disp.nest 2 fieldsDoc)
  where
    fieldsDoc = showConfig fields sections fgSections cur
    argDoc
      | arg == "" = Disp.empty
      | otherwise = Disp.text arg

-- |
--
-- TODO: subsections
-- TODO: this should simply build 'PrettyField'
ppFgSection
  :: String
  -- ^ section name
  -> String
  -- ^ parameter
  -> FG.PrettyFieldGrammar a a
  -> a
  -> Disp.Doc
ppFgSection secName arg grammar x
  | null prettyFields = Disp.empty
  | otherwise =
      Disp.text secName
        <+> argDoc
        $+$ (Disp.nest 2 fieldsDoc)
  where
    prettyFields = FG.prettyFieldGrammar cabalSpecLatest grammar x

    argDoc
      | arg == "" = Disp.empty
      | otherwise = Disp.text arg

    fieldsDoc =
      Disp.vcat
        [ Disp.text fname' <<>> Disp.colon <<>> doc
        | F.PrettyField _ fname doc <- prettyFields -- TODO: this skips sections
        , let fname' = fromUTF8BS fname
        ]

-----------------------------------------------
-- Top level config file parsing and printing
--

-- | Parse a string in the config file syntax into a value, based on a
-- description of the configuration file in terms of its fields and sections.
--
-- It accumulates the result on top of a given initial (typically empty) value.
parseConfig
  :: [FieldDescr a]
  -> [SectionDescr a]
  -> [FGSectionDescr FG.ParsecFieldGrammar a]
  -> a
  -> BS.ByteString
  -> ParseResult a
parseConfig fieldDescrs sectionDescrs fgSectionDescrs empty str =
  parseFieldsAndSections fieldDescrs sectionDescrs fgSectionDescrs empty
    =<< readFields str

-- | Render a value in the config file syntax, based on a description of the
-- configuration file in terms of its fields and sections.
showConfig :: [FieldDescr a] -> [SectionDescr a] -> [FGSectionDescr FG.PrettyFieldGrammar a] -> a -> Disp.Doc
showConfig = ppFieldsAndSections