packages feed

hspretty-0.1.0.0: src/Formatters/CabalFmt.hs

{-# LANGUAGE OverloadedStrings #-}

-- |
-- Description : @cabal-fmt@ formatter.
--
-- Applies the @cabal-fmt@ formatter.
module Formatters.CabalFmt where

import qualified Data.Text.Short as ShortText
import Formatter (ErrorMessage, FileContent, Formatter, FormattingResult)
import qualified Formatter
import qualified Path
import qualified Path.IO
import PathFilter (PathFilter)
import qualified PathFilter
import qualified System.Directory
import qualified System.Exit
import qualified System.IO.Unsafe (unsafePerformIO)
import qualified System.Process
import qualified UnliftIO.IO

-- | cabal-fmt formatter.
formatter :: Formatter
formatter = Formatter.Formatter $ \path ->
  case PathFilter.unPathFilter pfCabal path of
    PathFilter.Reject -> Formatter.DoNotFormat
    PathFilter.Accept -> Formatter.Format formatAction

-- | Formatting action; defers to a 'System.IO.Unsafe.unsafePerformIO' action.
formatAction :: FileContent -> FormattingResult FileContent
formatAction content = System.IO.Unsafe.unsafePerformIO $ formatActionIO content

-- | Formatting action in 'IO'.
formatActionIO :: FileContent -> IO (FormattingResult FileContent)
formatActionIO content = do
  -- check that we have the cabal-fmt utility installed first
  cabalFmtCheck <- checkForCabalFmt
  case cabalFmtCheck of
    Left err -> pure . Formatter.Error $ err
    Right () -> do
      -- create a temp file for the cabal file that must be formatted and
      --  invoke cabal-fmt on that
      Path.IO.withSystemTempFile ".cabal" $
        \tempFile handle -> do
          UnliftIO.IO.hClose handle
          writeResult <- Formatter.writeAbsoluteFile tempFile content
          case writeResult of
            Left errorMessage -> pure . Formatter.Error $ errorMessage
            Right () -> do
              let cp =
                    System.Process.proc
                      "cabal-fmt"
                      ["--inplace", Path.fromAbsFile tempFile]
              (exitCode, _, stderr) <-
                System.Process.readCreateProcessWithExitCode cp ""
              case exitCode of
                System.Exit.ExitFailure _ ->
                  pure
                    . Formatter.Error
                    . Formatter.ErrorMessage
                    . ShortText.pack
                    $ stderr
                System.Exit.ExitSuccess -> do
                  readResult <- Formatter.readAbsoluteFile tempFile
                  case readResult of
                    Left errorMessage -> pure . Formatter.Error $ errorMessage
                    Right outContent ->
                      pure $
                        if outContent == content
                          then Formatter.Unchanged
                          else Formatter.Changed outContent

-- | Check for the @cabal-fmt@ executable, producing an error message if it is
--   not installed.
checkForCabalFmt :: IO (Either ErrorMessage ())
checkForCabalFmt = do
  exe <- System.Directory.findExecutable "cabal-fmt"
  case exe of
    Nothing ->
      pure . Left . Formatter.ErrorMessage $
        "Could not find executable \"cabal-fmt\"; please make sure it is "
          <> "available on the path."
    Just _ -> pure . Right $ ()

-- | A 'PathFilter' for @.cabal@ files.
pfCabal :: PathFilter
pfCabal = PathFilter.pfExtension ".cabal"