fourmolu-0.5.0.0: src/Ormolu/Config.hs
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
-- | Configuration options used by the tool.
module Ormolu.Config
( Config (..),
ColorMode (..),
RegionIndices (..),
RegionDeltas (..),
SourceType (..),
defaultConfig,
PrinterOpts (..),
PrinterOptsPartial,
PrinterOptsTotal,
defaultPrinterOpts,
loadConfigFile,
configFileName,
ConfigFileLoadResult (..),
fillMissingPrinterOpts,
CommaStyle (..),
HaddockPrintStyle (..),
regionIndicesToDeltas,
DynOption (..),
dynOptionToLocatedStr,
)
where
import Data.Aeson
( FromJSON (..),
camelTo2,
constructorTagModifier,
defaultOptions,
fieldLabelModifier,
genericParseJSON,
)
import qualified Data.ByteString.Lazy as BS
import Data.Char (isLower)
import Data.Functor.Identity (Identity (..))
import Data.YAML (Pos)
import Data.YAML.Aeson (decode1)
import GHC.Generics (Generic)
import qualified GHC.Types.SrcLoc as GHC
import Ormolu.Terminal (ColorMode (..))
import System.Directory
( XdgDirectory (XdgConfig),
findFile,
getXdgDirectory,
makeAbsolute,
)
import System.FilePath (splitPath, (</>))
-- | Type of sources that can be formatted by Ormolu.
data SourceType
= -- | Consider the input as a regular Haskell module
ModuleSource
| -- | Consider the input as a Backpack module signature
SignatureSource
deriving (Eq, Show)
-- | Ormolu configuration.
data Config region = Config
{ -- | Dynamic options to pass to GHC parser
cfgDynOptions :: ![DynOption],
-- | Do formatting faster but without automatic detection of defects
cfgUnsafe :: !Bool,
-- | Output information useful for debugging
cfgDebug :: !Bool,
-- | Checks if re-formatting the result is idempotent
cfgCheckIdempotence :: !Bool,
-- | How to parse the input (regular haskell module or Backpack file)
cfgSourceType :: !SourceType,
-- | Whether to use colors and other features of ANSI terminals
cfgColorMode :: !ColorMode,
-- | Region selection
cfgRegion :: !region,
cfgPrinterOpts :: !PrinterOptsTotal
}
deriving (Eq, Show, Functor, Generic)
-- | Region selection as the combination of start and end line numbers.
data RegionIndices = RegionIndices
{ -- | Start line of the region to format
regionStartLine :: !(Maybe Int),
-- | End line of the region to format
regionEndLine :: !(Maybe Int)
}
deriving (Eq, Show)
-- | Region selection as the length of the literal prefix and the literal
-- suffix.
data RegionDeltas = RegionDeltas
{ -- | Prefix length in number of lines
regionPrefixLength :: !Int,
-- | Suffix length in number of lines
regionSuffixLength :: !Int
}
deriving (Eq, Show)
-- | Default @'Config' 'RegionIndices'@.
defaultConfig :: Config RegionIndices
defaultConfig =
Config
{ cfgDynOptions = [],
cfgUnsafe = False,
cfgDebug = False,
cfgCheckIdempotence = False,
cfgSourceType = ModuleSource,
cfgColorMode = Auto,
cfgRegion =
RegionIndices
{ regionStartLine = Nothing,
regionEndLine = Nothing
},
cfgPrinterOpts = defaultPrinterOpts
}
-- | Options controlling formatting output.
data PrinterOpts f = PrinterOpts
{ -- | Number of spaces to use for indentation
poIndentation :: f Int,
-- | Whether to place commas at start or end of lines
poCommaStyle :: f CommaStyle,
-- | Whether to indent `where` blocks
poIndentWheres :: f Bool,
-- | Leave space before opening record brace
poRecordBraceSpace :: f Bool,
-- | Trailing commas with parentheses on separate lines
poDiffFriendlyImportExport :: f Bool,
-- | Be less opinionated about spaces/newlines etc.
poRespectful :: f Bool,
-- | How to print doc comments
poHaddockStyle :: f HaddockPrintStyle,
-- | Number of newlines between top-level decls
poNewlinesBetweenDecls :: f Int
}
deriving (Generic)
-- | A version of 'PrinterOpts' where any field can be empty.
-- This corresponds to the information in a config file or in CLI options.
type PrinterOptsPartial = PrinterOpts Maybe
deriving instance Eq PrinterOptsPartial
deriving instance Show PrinterOptsPartial
instance Semigroup PrinterOptsPartial where
(<>) = fillMissingPrinterOpts
instance Monoid PrinterOptsPartial where
mempty = PrinterOpts Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
-- | A version of 'PrinterOpts' without empty fields.
type PrinterOptsTotal = PrinterOpts Identity
deriving instance Eq PrinterOptsTotal
deriving instance Show PrinterOptsTotal
defaultPrinterOpts :: PrinterOptsTotal
defaultPrinterOpts =
PrinterOpts
{ poIndentation = pure 4,
poCommaStyle = pure Leading,
poIndentWheres = pure False,
poRecordBraceSpace = pure False,
poDiffFriendlyImportExport = pure True,
poRespectful = pure True,
poHaddockStyle = pure HaddockMultiLine,
poNewlinesBetweenDecls = pure 1
}
-- | Fill the field values that are 'Nothing' in the first argument
-- with the values of the corresponding fields of the second argument.
fillMissingPrinterOpts ::
forall f.
Applicative f =>
PrinterOptsPartial ->
PrinterOpts f ->
PrinterOpts f
fillMissingPrinterOpts p1 p2 =
PrinterOpts
{ poIndentation = fillField poIndentation,
poCommaStyle = fillField poCommaStyle,
poIndentWheres = fillField poIndentWheres,
poRecordBraceSpace = fillField poRecordBraceSpace,
poDiffFriendlyImportExport = fillField poDiffFriendlyImportExport,
poRespectful = fillField poRespectful,
poHaddockStyle = fillField poHaddockStyle,
poNewlinesBetweenDecls = fillField poNewlinesBetweenDecls
}
where
fillField :: (forall g. PrinterOpts g -> g a) -> f a
fillField f = maybe (f p2) pure $ f p1
data CommaStyle
= Leading
| Trailing
deriving (Eq, Ord, Show, Generic, Bounded, Enum)
instance FromJSON CommaStyle where
parseJSON =
genericParseJSON
defaultOptions
{ constructorTagModifier = camelTo2 '-'
}
data HaddockPrintStyle
= HaddockSingleLine
| HaddockMultiLine
deriving (Eq, Ord, Show, Generic, Bounded, Enum)
instance FromJSON HaddockPrintStyle where
parseJSON =
genericParseJSON
defaultOptions
{ constructorTagModifier = drop (length "haddock-") . camelTo2 '-'
}
-- | Convert 'RegionIndices' into 'RegionDeltas'.
regionIndicesToDeltas ::
-- | Total number of lines in the input
Int ->
-- | Region indices
RegionIndices ->
-- | Region deltas
RegionDeltas
regionIndicesToDeltas total RegionIndices {..} =
RegionDeltas
{ regionPrefixLength = maybe 0 (subtract 1) regionStartLine,
regionSuffixLength = maybe 0 (total -) regionEndLine
}
-- | A wrapper for dynamic options.
newtype DynOption = DynOption
{ unDynOption :: String
}
deriving (Eq, Ord, Show)
-- | Convert 'DynOption' to @'GHC.Located' 'String'@.
dynOptionToLocatedStr :: DynOption -> GHC.Located String
dynOptionToLocatedStr (DynOption o) = GHC.L GHC.noSrcSpan o
instance FromJSON PrinterOptsPartial where
parseJSON =
genericParseJSON
defaultOptions
{ fieldLabelModifier = camelTo2 '-' . dropWhile isLower
}
-- | Read options from a config file, if found.
-- Looks recursively in parent folders, then in 'XdgConfig',
-- for a file named /fourmolu.yaml/.
loadConfigFile :: FilePath -> IO ConfigFileLoadResult
loadConfigFile path = do
root <- makeAbsolute path
xdg <- getXdgDirectory XdgConfig ""
let dirs = reverse $ xdg : scanl1 (</>) (splitPath root)
findFile dirs configFileName >>= \case
Nothing -> return $ ConfigNotFound dirs
Just file ->
either (ConfigParseError file) (ConfigLoaded file)
. decode1
<$> BS.readFile file
-- | The result of calling 'loadConfigFile'.
data ConfigFileLoadResult
= ConfigLoaded FilePath PrinterOptsPartial
| ConfigParseError FilePath (Pos, String)
| ConfigNotFound [FilePath]
deriving (Eq, Show)
-- | Expected file name for YAML config.
configFileName :: FilePath
configFileName = "fourmolu.yaml"