fourmolu-0.2.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 (..),
RegionIndices (..),
RegionDeltas (..),
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 SrcLoc as GHC
import System.Directory
( XdgDirectory (XdgConfig),
findFile,
getXdgDirectory,
makeAbsolute,
)
import System.FilePath (splitPath, (</>))
-- | 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,
-- | Region selection
cfgRegion :: !region,
cfgPrinterOpts :: !PrinterOptsTotal
}
deriving (Eq, Show, Functor)
-- | 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,
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
}
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
-- | 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
}
-- | 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
}
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)
instance FromJSON CommaStyle where
parseJSON =
genericParseJSON
defaultOptions
{ constructorTagModifier = camelTo2 '-'
}
data HaddockPrintStyle
= HaddockSingleLine
| HaddockMultiLine
deriving (Eq, Ord, Show, Generic)
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"