{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE LambdaCase #-}
module Ide.Plugin.Brittany where
import Control.Exception (bracket_)
import Control.Lens
import Control.Monad.IO.Class
import Control.Monad.Trans.Maybe (MaybeT, runMaybeT)
import Data.Maybe (mapMaybe, maybeToList, fromMaybe)
import Data.Semigroup
import Data.Text (Text)
import qualified Data.Text as T
import Development.IDE hiding (pluginHandlers)
import Development.IDE.GHC.Compat (ModSummary (ms_hspp_opts), topDir)
import qualified DynFlags as D
import qualified EnumSet as S
import GHC.LanguageExtensions.Type
import Ide.PluginUtils
import Ide.Types
import Language.Haskell.Brittany
import Language.LSP.Types as J
import qualified Language.LSP.Types.Lens as J
import System.Environment (setEnv, unsetEnv)
import System.FilePath
-- These imports are for the temporary pPrintText & can be removed when
-- issue #2005 is resolved
import Language.Haskell.Brittany.Internal.Config.Types
import Language.Haskell.Brittany.Internal
import Language.Haskell.Brittany.Internal.Types
import Language.Haskell.Brittany.Internal.Utils
import Language.Haskell.Brittany.Internal.Obfuscation
import Language.Haskell.Brittany.Internal.Config
import Data.CZipWith
import Control.Monad.Trans.Class (lift)
import qualified Control.Monad.Trans.Except as ExceptT
import qualified Data.List as List
import qualified Data.Text as Text
import qualified Language.Haskell.GHC.ExactPrint as ExactPrint
import qualified Language.Haskell.GHC.ExactPrint.Types as ExactPrint
import qualified Data.Text.Lazy as TextL
import qualified DynFlags as GHC
import qualified GHC
import qualified GHC.LanguageExtensions.Type as GHC
descriptor :: PluginId -> PluginDescriptor IdeState
descriptor plId = (defaultPluginDescriptor plId)
{ pluginHandlers = mkFormattingHandlers provider
}
-- | Formatter provider of Brittany.
-- Formats the given source in either a given Range or the whole Document.
-- If the provider fails an error is returned that can be displayed to the user.
provider :: FormattingHandler IdeState
provider ide typ contents nfp opts = liftIO $ do
confFile <- getConfFile nfp
let (range, selectedContents) = case typ of
FormatText -> (fullRange contents, contents)
FormatRange r -> (normalize r, extractRange r contents)
modsum <- fmap msrModSummary $ runAction "brittany" ide $ use_ GetModSummaryWithoutTimestamps nfp
let dflags = ms_hspp_opts modsum
let withRuntimeLibdir = bracket_ (setEnv key $ topDir dflags) (unsetEnv key)
where key = "GHC_EXACTPRINT_GHC_LIBDIR"
res <- withRuntimeLibdir $ formatText dflags confFile opts selectedContents
case res of
Left err -> return $ Left $ responseError (T.pack $ "brittanyCmd: " ++ unlines (map showErr err))
Right newText -> return $ Right $ J.List [TextEdit range newText]
-- | Primitive to format text with the given option.
-- May not throw exceptions but return a Left value.
-- Errors may be presented to the user.
formatText
:: MonadIO m
=> D.DynFlags
-> Maybe FilePath -- ^ Path to configs. If Nothing, default configs will be used.
-> FormattingOptions -- ^ Options for the formatter such as indentation.
-> Text -- ^ Text to format
-> m (Either [BrittanyError] Text) -- ^ Either formatted Text or a error from Brittany.
formatText df confFile opts text =
liftIO $ runBrittany tabSize df confFile text
where tabSize = opts ^. J.tabSize
-- | Recursively search in every directory of the given filepath for brittany.yaml.
-- If no such file has been found, return Nothing.
getConfFile :: NormalizedFilePath -> IO (Maybe FilePath)
getConfFile = findLocalConfigPath . takeDirectory . fromNormalizedFilePath
-- | Run Brittany on the given text with the given tab size and
-- a configuration path. If no configuration path is given, a
-- default configuration is chosen. The configuration may overwrite
-- tab size parameter.
--
-- Returns either a list of Brittany Errors or the reformatted text.
-- May not throw an exception.
runBrittany :: Int -- ^ tab size
-> D.DynFlags
-> Maybe FilePath -- ^ local config file
-> Text -- ^ text to format
-> IO (Either [BrittanyError] Text)
runBrittany tabSize df confPath text = do
let cfg = mempty
{ _conf_layout =
mempty { _lconfig_indentAmount = opt (Last tabSize)
}
, _conf_forward =
(mempty :: CForwardOptions Option)
{ _options_ghc = opt (getExtensions df)
}
}
config <- fromMaybeT (pure staticDefaultConfig) (readConfigsWithUserConfig cfg (maybeToList confPath))
(errsAndWarnings, resultText) <- pPrintText config text
if any isError errsAndWarnings then
return $ Left errsAndWarnings
else
return $ Right resultText
fromMaybeT :: Monad m => m a -> MaybeT m a -> m a
fromMaybeT def act = runMaybeT act >>= maybe def return
opt :: a -> Option a
opt = Option . Just
showErr :: BrittanyError -> String
showErr (ErrorInput s) = s
showErr (ErrorMacroConfig err input)
= "Error: parse error in inline configuration: " ++ err ++ " in the string \"" ++ input ++ "\"."
showErr (ErrorUnusedComment s) = s
showErr (LayoutWarning s) = s
showErr (ErrorUnknownNode s _) = s
showErr ErrorOutputCheck = "Brittany error - invalid output"
showExtension :: Extension -> Maybe String
showExtension Cpp = Just "-XCPP"
-- Brittany chokes on parsing extensions that produce warnings
showExtension DatatypeContexts = Nothing
showExtension RecordPuns = Just "-XNamedFieldPuns"
showExtension other = Just $ "-X" ++ show other
getExtensions :: D.DynFlags -> [String]
getExtensions = mapMaybe showExtension . S.toList . D.extensionFlags
-- | This is a temporary fix that allows us to format the text if brittany
-- throws warnings during pretty printing.
--
-- It should be removed when our PR to brittany is merged + released.
-- See:
-- - https://github.com/haskell/haskell-language-server/issues/2005
-- - https://github.com/lspitzner/brittany/pull/351
pPrintText
:: Config -- ^ global program config
-> Text -- ^ input text
-> IO ([BrittanyError], Text) -- ^ list of errors/warnings & result text
pPrintText config text =
fmap (either id id) . ExceptT.runExceptT $ do
let ghcOptions = config & _conf_forward & _options_ghc & runIdentity
-- there is a good of code duplication between the following code and the
-- `pureModuleTransform` function. Unfortunately, there are also a good
-- amount of slight differences: This module is a bit more verbose, and
-- it tries to use the full-blown `parseModule` function which supports
-- CPP (but requires the input to be a file..).
let cppMode = config & _conf_preprocessor & _ppconf_CPPMode & confUnpack
-- the flag will do the following: insert a marker string
-- ("-- BRITANY_INCLUDE_HACK ") right before any lines starting with
-- "#include" before processing (parsing) input; and remove that marker
-- string from the transformation output.
-- The flag is intentionally misspelled to prevent clashing with
-- inline-config stuff.
let hackAroundIncludes =
config & _conf_preprocessor & _ppconf_hackAroundIncludes & confUnpack
let exactprintOnly = viaGlobal || viaDebug
where
viaGlobal = config & _conf_roundtrip_exactprint_only & confUnpack
viaDebug =
config & _conf_debug & _dconf_roundtrip_exactprint_only & confUnpack
let cppCheckFunc dynFlags = if GHC.xopt GHC.Cpp dynFlags
then case cppMode of
CPPModeAbort ->
return $ Left "Encountered -XCPP. Aborting."
CPPModeWarn ->
return $ Right True
CPPModeNowarn ->
return $ Right True
else return $ Right False
parseResult <- do
-- TODO: refactor this hack to not be mixed into parsing logic
let hackF s = if "#include" `List.isPrefixOf` s
then "-- BRITANY_INCLUDE_HACK " ++ s
else s
let hackTransform = if hackAroundIncludes && not exactprintOnly
then List.intercalate "\n" . fmap hackF . lines'
else id
liftIO $ parseModuleFromString ghcOptions
"stdin"
cppCheckFunc
(hackTransform $ Text.unpack text)
case parseResult of
Left left -> do
ExceptT.throwE ([ErrorInput left], text)
Right (anns, parsedSource, hasCPP) -> do
(inlineConf, perItemConf) <-
case
extractCommentConfigs anns (getTopLevelDeclNameMap parsedSource)
of
Left (err, input) -> do
let errMsg =
"Error: parse error in inline configuration: "
<> err
<> " in the string \""
<> input
<> "\"."
ExceptT.throwE ([ErrorInput errMsg], text)
Right c ->
pure c
let moduleConf = cZipWith fromOptionIdentity config inlineConf
let disableFormatting =
moduleConf & _conf_disable_formatting & confUnpack
(errsWarns, outSText, _) <- do
if
| disableFormatting -> do
pure ([], text, False)
| exactprintOnly -> do
let r = Text.pack $ ExactPrint.exactPrint parsedSource anns
pure ([], r, r /= text)
| otherwise -> do
(ews, outRaw) <- if hasCPP
then return
$ pPrintModule moduleConf perItemConf anns parsedSource
else liftIO $ pPrintModuleAndCheck moduleConf
perItemConf
anns
parsedSource
let hackF s = fromMaybe s $ TextL.stripPrefix
(TextL.pack "-- BRITANY_INCLUDE_HACK ")
s
let out = TextL.toStrict $ if hackAroundIncludes
then
TextL.intercalate (TextL.pack "\n")
$ hackF
<$> TextL.splitOn (TextL.pack "\n") outRaw
else outRaw
out' <- if moduleConf & _conf_obfuscate & confUnpack
then lift $ obfuscate out
else pure out
pure (ews, out', out' /= text)
let customErrOrder ErrorInput{} = 4
customErrOrder LayoutWarning{} = -1 :: Int
customErrOrder ErrorOutputCheck{} = 1
customErrOrder ErrorUnusedComment{} = 2
customErrOrder ErrorUnknownNode{} = -2 :: Int
customErrOrder ErrorMacroConfig{} = 5
hasErrors =
if config & _conf_errorHandling & _econf_Werror & confUnpack
then not $ null errsWarns
else 0 < maximum (-1 : fmap customErrOrder errsWarns)
return (errsWarns, if hasErrors then text else outSText)
isError :: BrittanyError -> Bool
isError = \case
LayoutWarning{} -> False
ErrorUnknownNode{} -> False
_ -> True