lib: add a module for custom parse errors
... for displaying the source line on which parse errors occured Over the following set of commits, I will to refactor the parsers to obviate the `ExceptT String` layer of the `ErroringJournalParser` type so that all parse errors go through Megaparsec's parse error machinery.
This commit is contained in:
		
							parent
							
								
									1624206b5d
								
							
						
					
					
						commit
						ca201e4618
					
				| @ -13,6 +13,7 @@ import Text.Megaparsec.Char | |||||||
| import Text.Printf | import Text.Printf | ||||||
| 
 | 
 | ||||||
| import Hledger.Data.Types | import Hledger.Data.Types | ||||||
|  | import Hledger.Utils.ParseErrors | ||||||
| import Hledger.Utils.UTF8IOCompat (error') | import Hledger.Utils.UTF8IOCompat (error') | ||||||
| 
 | 
 | ||||||
| -- | A parser of string to some type. | -- | A parser of string to some type. | ||||||
|  | |||||||
							
								
								
									
										218
									
								
								hledger-lib/Hledger/Utils/ParseErrors.hs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								hledger-lib/Hledger/Utils/ParseErrors.hs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,218 @@ | |||||||
|  | {-# LANGUAGE BangPatterns #-} | ||||||
|  | {-# LANGUAGE FlexibleContexts #-} | ||||||
|  | {-# LANGUAGE LambdaCase #-} | ||||||
|  | {-# LANGUAGE ScopedTypeVariables #-} | ||||||
|  | {-# LANGUAGE StandaloneDeriving #-} | ||||||
|  | 
 | ||||||
|  | module Hledger.Utils.ParseErrors ( | ||||||
|  |   -- * Custom parse error type | ||||||
|  |   CustomErr, | ||||||
|  | 
 | ||||||
|  |   -- * Throwing custom parse errors | ||||||
|  |   parseErrorAt, | ||||||
|  |   parseErrorAtRegion, | ||||||
|  |   withSource, | ||||||
|  | 
 | ||||||
|  |   -- * Pretty-printing custom parse errors | ||||||
|  |   customParseErrorPretty | ||||||
|  | ) | ||||||
|  | where | ||||||
|  | 
 | ||||||
|  | import Data.Foldable (asum, toList) | ||||||
|  | import qualified Data.List.NonEmpty as NE | ||||||
|  | import Data.Proxy (Proxy (Proxy)) | ||||||
|  | import qualified Data.Set as S | ||||||
|  | import Data.Text (Text) | ||||||
|  | import Data.Void (Void) | ||||||
|  | import Text.Megaparsec | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | --- * Custom parse error type | ||||||
|  | 
 | ||||||
|  | -- | A custom error type for the parser. The type is specialized to | ||||||
|  | -- parsers of 'Text' streams. | ||||||
|  | 
 | ||||||
|  | data CustomErr | ||||||
|  |   -- | Fail with a message at a specific source position interval. The | ||||||
|  |   -- interval must be contained within a single line. | ||||||
|  |   = ErrorFailAt SourcePos -- ^ Starting position | ||||||
|  |                 Pos -- ^ Ending position (column; same line as start) | ||||||
|  |                 String -- ^ Error message | ||||||
|  |   -- | Attach a source file to a parse error (for error reporting from | ||||||
|  |   -- include files, e.g. with the 'region' parser combinator) | ||||||
|  |   | ErrorWithSource Text -- ^ Source file contents | ||||||
|  |                     (ParseError Char CustomErr) -- ^ The original  | ||||||
|  |   deriving (Show, Eq, Ord) | ||||||
|  | 
 | ||||||
|  | -- We require an 'Ord' instance for 'CustomError' so that they may be | ||||||
|  | -- stored in a 'Set'. The actual instance is inconsequential, so we just | ||||||
|  | -- derive it, but this requires an (orphan) instance for 'ParseError'. | ||||||
|  | -- Hopefully this does not cause any trouble. | ||||||
|  | 
 | ||||||
|  | deriving instance (Ord c, Ord e) => Ord (ParseError c e) | ||||||
|  | 
 | ||||||
|  | instance ShowErrorComponent CustomErr where | ||||||
|  |   showErrorComponent (ErrorFailAt _ _ errMsg) = errMsg | ||||||
|  |   showErrorComponent (ErrorWithSource _ e) = parseErrorTextPretty e | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | --- * Throwing custom parse errors | ||||||
|  | 
 | ||||||
|  | -- | Fail at a specific source position. | ||||||
|  | 
 | ||||||
|  | parseErrorAt :: MonadParsec CustomErr s m => SourcePos -> String -> m a | ||||||
|  | parseErrorAt pos msg = customFailure (ErrorFailAt pos (sourceColumn pos) msg) | ||||||
|  | {-# INLINABLE parseErrorAt #-} | ||||||
|  | 
 | ||||||
|  | -- | Fail at a specific source interval (within a single line). The | ||||||
|  | -- interval is inclusive on the left and exclusive on the right; that is, | ||||||
|  | -- it spans from the start position to just before (and not including) the | ||||||
|  | -- end position. | ||||||
|  | 
 | ||||||
|  | parseErrorAtRegion | ||||||
|  |   :: MonadParsec CustomErr s m | ||||||
|  |   => SourcePos -- ^ Start position | ||||||
|  |   -> SourcePos -- ^ End position | ||||||
|  |   -> String    -- ^ Error message | ||||||
|  |   -> m a | ||||||
|  | parseErrorAtRegion startPos endPos msg = | ||||||
|  |   let startCol = sourceColumn startPos | ||||||
|  |       endCol' = mkPos $ subtract 1 $ unPos $ sourceColumn endPos | ||||||
|  |       endCol = if startCol <= endCol' | ||||||
|  |                     && sourceLine startPos == sourceLine endPos | ||||||
|  |                then endCol' else startCol | ||||||
|  |   in  customFailure (ErrorFailAt startPos endCol msg) | ||||||
|  | {-# INLINABLE parseErrorAtRegion #-} | ||||||
|  | 
 | ||||||
|  | -- | Attach a source file to a parse error. Intended for use with the | ||||||
|  | -- 'region' parser combinator. | ||||||
|  | 
 | ||||||
|  | withSource :: Text -> ParseError Char CustomErr -> ParseError Char CustomErr | ||||||
|  | withSource s e = | ||||||
|  |   FancyError (errorPos e) $ S.singleton $ ErrorCustom $ ErrorWithSource s e | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | --- * Pretty-printing custom parse errors | ||||||
|  | 
 | ||||||
|  | -- | Pretty-print our custom parse errors and display the line on which | ||||||
|  | -- the parse error occured. Use this instead of 'parseErrorPretty'. | ||||||
|  | -- | ||||||
|  | -- If any custom errors are present, arbitrarily take the first one (since | ||||||
|  | -- only one custom error should be used at a time). | ||||||
|  | 
 | ||||||
|  | customParseErrorPretty :: Text -> ParseError Char CustomErr -> String | ||||||
|  | customParseErrorPretty source err = case findCustomError err of | ||||||
|  |   Nothing -> customParseErrorPretty' source err pos1 | ||||||
|  | 
 | ||||||
|  |   Just (ErrorWithSource customSource customErr) -> | ||||||
|  |     customParseErrorPretty customSource customErr | ||||||
|  | 
 | ||||||
|  |   Just (ErrorFailAt sourcePos col errMsg) -> | ||||||
|  |     let newPositionStack = sourcePos NE.:| NE.tail (errorPos err) | ||||||
|  |         errorIntervalLength = mkPos $ max 1 $ | ||||||
|  |           unPos col - unPos (sourceColumn sourcePos) + 1 | ||||||
|  | 
 | ||||||
|  |         newErr :: ParseError Char Void | ||||||
|  |         newErr = FancyError newPositionStack (S.singleton (ErrorFail errMsg)) | ||||||
|  | 
 | ||||||
|  |     in  customParseErrorPretty' source newErr errorIntervalLength | ||||||
|  | 
 | ||||||
|  |   where | ||||||
|  |     findCustomError :: ParseError Char CustomErr -> Maybe CustomErr | ||||||
|  |     findCustomError err = case err of | ||||||
|  |       FancyError _ errSet ->  | ||||||
|  |         finds (\case {ErrorCustom e -> Just e; _ -> Nothing}) errSet | ||||||
|  |       _ -> Nothing | ||||||
|  | 
 | ||||||
|  |     finds :: (Foldable t) => (a -> Maybe b) -> t a -> Maybe b | ||||||
|  |     finds f = asum . map f . toList | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | --- * Modified Megaparsec source | ||||||
|  | 
 | ||||||
|  | -- The below code has been copied from the Megaparsec module and modified | ||||||
|  | -- to suit our needs. Changes are marked with '-- *'. | ||||||
|  | -- | ||||||
|  | -- NOTE: I am not sure what we are now obligated to do, having directly | ||||||
|  | -- copied source code from another project. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | -- | Pretty-print a 'ParseError Char CustomErr' and display the line on | ||||||
|  | -- which the parse error occurred. The rendered 'String' always ends with | ||||||
|  | -- a newline. | ||||||
|  | 
 | ||||||
|  | customParseErrorPretty' | ||||||
|  |   :: ( ShowToken (Token s) | ||||||
|  |      , LineToken (Token s) | ||||||
|  |      , ShowErrorComponent e | ||||||
|  |      , Stream s ) | ||||||
|  |   => s                 -- ^ Original input stream | ||||||
|  |   -> ParseError (Token s) e -- ^ Parse error to render | ||||||
|  |   -> Pos               -- ^ Length of error interval -- * added | ||||||
|  |   -> String            -- ^ Result of rendering | ||||||
|  | customParseErrorPretty' = customParseErrorPretty_ defaultTabWidth | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | customParseErrorPretty_ | ||||||
|  |   :: forall s e. | ||||||
|  |      ( ShowToken (Token s) | ||||||
|  |      , LineToken (Token s) | ||||||
|  |      , ShowErrorComponent e | ||||||
|  |      , Stream s ) | ||||||
|  |   => Pos               -- ^ Tab width | ||||||
|  |   -> s                 -- ^ Original input stream | ||||||
|  |   -> ParseError (Token s) e -- ^ Parse error to render | ||||||
|  |   -> Pos               -- ^ Length of error interval -- * added | ||||||
|  |   -> String            -- ^ Result of rendering | ||||||
|  | customParseErrorPretty_ w s e l = | ||||||
|  |   sourcePosStackPretty (errorPos e) <> ":\n" <> | ||||||
|  |     padding <> "|\n" <> | ||||||
|  |     lineNumber <> " | " <> rline <> "\n" <> | ||||||
|  |     padding <> "| " <> rpadding <> highlight <> "\n" <> | ||||||
|  |     parseErrorTextPretty e | ||||||
|  |   where | ||||||
|  |     epos       = NE.head (errorPos e) -- * changed from NE.last to NE.head | ||||||
|  |     lineNumber = (show . unPos . sourceLine) epos | ||||||
|  |     padding    = replicate (length lineNumber + 1) ' ' | ||||||
|  |     rpadding   = replicate (unPos (sourceColumn epos) - 1) ' ' | ||||||
|  |     highlight  = replicate (unPos l) '^' | ||||||
|  |     rline      = | ||||||
|  |       case rline' of | ||||||
|  |         [] -> "<empty line>" | ||||||
|  |         xs -> expandTab w xs | ||||||
|  |     rline'     = fmap tokenAsChar . chunkToTokens (Proxy :: Proxy s) $ | ||||||
|  |       selectLine (sourceLine epos) s | ||||||
|  | 
 | ||||||
|  | -- | Select a line from input stream given its number. | ||||||
|  | 
 | ||||||
|  | selectLine | ||||||
|  |   :: forall s. (LineToken (Token s), Stream s) | ||||||
|  |   => Pos               -- ^ Number of line to select | ||||||
|  |   -> s                 -- ^ Input stream | ||||||
|  |   -> Tokens s          -- ^ Selected line | ||||||
|  | selectLine l = go pos1 | ||||||
|  |   where | ||||||
|  |     go !n !s = | ||||||
|  |       if n == l | ||||||
|  |         then fst (takeWhile_ notNewline s) | ||||||
|  |         else go (n <> pos1) (stripNewline $ snd (takeWhile_ notNewline s)) | ||||||
|  |     notNewline = not . tokenIsNewline | ||||||
|  |     stripNewline s = | ||||||
|  |       case take1_ s of | ||||||
|  |         Nothing -> s | ||||||
|  |         Just (_, s') -> s' | ||||||
|  | 
 | ||||||
|  | -- | Replace tab characters with given number of spaces. | ||||||
|  | 
 | ||||||
|  | expandTab | ||||||
|  |   :: Pos | ||||||
|  |   -> String | ||||||
|  |   -> String | ||||||
|  | expandTab w' = go 0 | ||||||
|  |   where | ||||||
|  |     go 0 []        = [] | ||||||
|  |     go 0 ('\t':xs) = go w xs | ||||||
|  |     go 0 (x:xs)    = x : go 0 xs | ||||||
|  |     go !n xs       = ' ' : go (n - 1) xs | ||||||
|  |     w              = unPos w' | ||||||
|  | 
 | ||||||
| @ -2,7 +2,7 @@ | |||||||
| -- | -- | ||||||
| -- see: https://github.com/sol/hpack | -- see: https://github.com/sol/hpack | ||||||
| -- | -- | ||||||
| -- hash: 4e9f93f0ca43f594b381f1e1e03e67ce3379bd4830b260e6f7dc1596b946993f | -- hash: fbcc49317255a91cf8d9795d99203ce5e9930e4981b2fc246349a6ea8d23af74 | ||||||
| 
 | 
 | ||||||
| name:           hledger-lib | name:           hledger-lib | ||||||
| version:        1.9.99 | version:        1.9.99 | ||||||
| @ -95,6 +95,7 @@ library | |||||||
|       Hledger.Utils.UTF8IOCompat |       Hledger.Utils.UTF8IOCompat | ||||||
|       Text.Tabular.AsciiWide |       Text.Tabular.AsciiWide | ||||||
|   other-modules: |   other-modules: | ||||||
|  |       Hledger.Utils.ParseErrors | ||||||
|       Paths_hledger_lib |       Paths_hledger_lib | ||||||
|   hs-source-dirs: |   hs-source-dirs: | ||||||
|       ./. |       ./. | ||||||
| @ -181,6 +182,7 @@ test-suite doctests | |||||||
|       Hledger.Utils.Color |       Hledger.Utils.Color | ||||||
|       Hledger.Utils.Debug |       Hledger.Utils.Debug | ||||||
|       Hledger.Utils.Parse |       Hledger.Utils.Parse | ||||||
|  |       Hledger.Utils.ParseErrors | ||||||
|       Hledger.Utils.Regex |       Hledger.Utils.Regex | ||||||
|       Hledger.Utils.String |       Hledger.Utils.String | ||||||
|       Hledger.Utils.Test |       Hledger.Utils.Test | ||||||
| @ -277,6 +279,7 @@ test-suite easytests | |||||||
|       Hledger.Utils.Color |       Hledger.Utils.Color | ||||||
|       Hledger.Utils.Debug |       Hledger.Utils.Debug | ||||||
|       Hledger.Utils.Parse |       Hledger.Utils.Parse | ||||||
|  |       Hledger.Utils.ParseErrors | ||||||
|       Hledger.Utils.Regex |       Hledger.Utils.Regex | ||||||
|       Hledger.Utils.String |       Hledger.Utils.String | ||||||
|       Hledger.Utils.Test |       Hledger.Utils.Test | ||||||
| @ -373,6 +376,7 @@ test-suite hunittests | |||||||
|       Hledger.Utils.Color |       Hledger.Utils.Color | ||||||
|       Hledger.Utils.Debug |       Hledger.Utils.Debug | ||||||
|       Hledger.Utils.Parse |       Hledger.Utils.Parse | ||||||
|  |       Hledger.Utils.ParseErrors | ||||||
|       Hledger.Utils.Regex |       Hledger.Utils.Regex | ||||||
|       Hledger.Utils.String |       Hledger.Utils.String | ||||||
|       Hledger.Utils.Test |       Hledger.Utils.Test | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user