packages feed

ormolu-0.9.0.0: src/Ormolu/Exception.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QualifiedDo #-}

-- | 'OrmoluException' type and surrounding definitions.
module Ormolu.Exception
  ( OrmoluException (..),
    printOrmoluException,
    withPrettyOrmoluExceptions,
  )
where

import Control.Exception
import Data.Foldable (for_)
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NE
import Data.Text (Text)
import Data.Text qualified as T
import Data.Void (Void)
import Distribution.Parsec.Error (PError, showPError)
import GHC.Types.SrcLoc
import Ormolu.Comments.Invariants (InvariantViolation, renderInvariantViolation)
import Ormolu.Diff.Text (TextDiff, printTextDiff)
import Ormolu.Terminal
import Ormolu.Terminal.QualifiedDo qualified as Term
import System.Exit (ExitCode (..))
import System.IO
import Text.Megaparsec (ParseErrorBundle, errorBundlePretty)

-- | Ormolu exception representing all cases when Ormolu can fail.
data OrmoluException
  = -- | Parsing of original source code failed
    OrmoluParsingFailed SrcSpan String
  | -- | Parsing of formatted source code failed
    OrmoluOutputParsingFailed SrcSpan String
  | -- | Original and resulting ASTs differ
    OrmoluASTDiffers TextDiff [RealSrcSpan]
  | -- | Formatted source code is not idempotent
    OrmoluNonIdempotentOutput TextDiff
  | -- | The comments that came out do not correspond to the comments that
    -- went in
    OrmoluCommentInvariantsViolated FilePath [InvariantViolation]
  | -- | Some GHC options were not recognized
    OrmoluUnrecognizedOpts (NonEmpty String)
  | -- | Cabal file parsing failed
    OrmoluCabalFileParsingFailed FilePath (NonEmpty PError)
  | -- | Missing input file path when using stdin input and
    -- accounting for .cabal files
    OrmoluMissingStdinInputFile
  | -- | A parse error in a fixity overrides file
    OrmoluFixityOverridesParseError (ParseErrorBundle Text Void)
  deriving (Show)

instance Exception OrmoluException where
  displayException = T.unpack . runTermPure . printOrmoluException

-- | Print an 'OrmoluException'.
printOrmoluException ::
  OrmoluException ->
  Term
printOrmoluException = \case
  OrmoluParsingFailed s e -> Term.do
    bold (putOutputable s)
    newline
    put "  The GHC parser (in Haddock mode) failed:"
    newline
    put "  "
    put (T.pack e)
    newline
  OrmoluOutputParsingFailed s e -> Term.do
    bold (putOutputable s)
    newline
    put "  Parsing of formatted code failed:"
    newline
    put "  "
    put (T.pack e)
    newline
  OrmoluASTDiffers diff ss -> Term.do
    printTextDiff diff
    newline
    put "  AST of input and AST of formatted code differ."
    newline
    for_ ss $ \s -> Term.do
      put "    at "
      putOutputable s
      newline
    put "  Please, consider reporting the bug."
    newline
    put "  To format anyway, use --unsafe."
    newline
  OrmoluNonIdempotentOutput diff -> Term.do
    printTextDiff diff
    newline
    put "  Formatting is not idempotent."
    newline
    put "  Please, consider reporting the bug."
    newline
  OrmoluCommentInvariantsViolated path violations -> Term.do
    put (T.pack path)
    newline
    for_ violations $ \violation -> Term.do
      put "  "
      put (renderInvariantViolation violation)
      newline
    newline
    put "  The comments of the output do not correspond to the comments of"
    newline
    put "  the input."
    newline
    put "  Please, consider reporting the bug."
    newline
    put "  To format anyway, use --unsafe."
    newline
  OrmoluUnrecognizedOpts opts -> Term.do
    put "The following GHC options were not recognized:"
    newline
    put "  "
    (put . T.unwords . map T.pack . NE.toList) opts
    newline
  OrmoluCabalFileParsingFailed cabalFile parseErrors -> Term.do
    put "Parsing this .cabal file failed:"
    newline
    for_ parseErrors $ \e -> Term.do
      put . T.pack $ "  " <> showPError cabalFile e
      newline
  OrmoluMissingStdinInputFile -> Term.do
    put "The --stdin-input-file option is necessary when using input"
    newline
    put "from stdin and accounting for .cabal files."
    newline
  OrmoluFixityOverridesParseError errorBundle -> Term.do
    put . T.pack . errorBundlePretty $ errorBundle
    newline

-- | Inside this wrapper, 'OrmoluException' will be caught and displayed
-- nicely.
withPrettyOrmoluExceptions ::
  -- | Color mode
  ColorMode ->
  -- | Action that may throw an exception
  IO ExitCode ->
  IO ExitCode
withPrettyOrmoluExceptions colorMode m = m `catch` h
  where
    h e = do
      runTerm (printOrmoluException e) colorMode stderr
      return . ExitFailure $
        case e of
          -- Error code 1 is for 'error' or 'notImplemented'.
          -- 2 used to be for erroring out on CPP.
          OrmoluParsingFailed {} -> 3
          OrmoluOutputParsingFailed {} -> 4
          OrmoluASTDiffers {} -> 5
          OrmoluNonIdempotentOutput {} -> 6
          OrmoluCommentInvariantsViolated {} -> 11
          OrmoluUnrecognizedOpts {} -> 7
          OrmoluCabalFileParsingFailed {} -> 8
          OrmoluMissingStdinInputFile {} -> 9
          OrmoluFixityOverridesParseError {} -> 10