2022-05-09 11:56:47 +03:00
{- |
Helpers for making error messages .
- }
{- # LANGUAGE OverloadedStrings # -}
2023-02-04 21:44:05 +03:00
{- # LANGUAGE RecordWildCards # -}
2022-05-09 11:56:47 +03:00
2022-05-09 21:45:27 +03:00
module Hledger.Data.Errors (
2023-02-04 21:44:05 +03:00
makeAccountTagErrorExcerpt ,
2022-05-09 11:56:47 +03:00
makeTransactionErrorExcerpt ,
makePostingErrorExcerpt ,
2022-08-01 18:30:39 +03:00
makePostingAccountErrorExcerpt ,
2022-07-29 11:01:34 +03:00
makeBalanceAssertionErrorExcerpt ,
2022-05-09 11:56:47 +03:00
transactionFindPostingIndex ,
)
where
import Data.Function ( ( & ) )
import Data.List ( find )
import Data.Text ( Text )
import qualified Data.Text as T
2022-05-09 21:45:27 +03:00
import Hledger.Data.Transaction ( showTransaction )
2024-01-23 20:58:26 +03:00
import Hledger.Data.Posting ( postingStripCosts )
2022-05-09 21:45:27 +03:00
import Hledger.Data.Types
2022-05-09 11:56:47 +03:00
import Hledger.Utils
2022-07-29 11:01:34 +03:00
import Data.Maybe
import Safe ( headMay )
2022-08-01 18:30:39 +03:00
import Hledger.Data.Posting ( isVirtual )
2022-05-09 11:56:47 +03:00
2023-02-04 21:44:05 +03:00
-- | Given an account name and its account directive, and a problem tag within the latter:
-- render it as a megaparsec-style excerpt, showing the original line number and
-- marked column or region.
-- Returns the file path, line number, column(s) if known,
-- and the rendered excerpt, or as much of these as is possible.
-- The returned columns will be accurate for the rendered error message but not for the original journal data.
makeAccountTagErrorExcerpt :: ( AccountName , AccountDeclarationInfo ) -> TagName -> ( FilePath , Int , Maybe ( Int , Maybe Int ) , Text )
makeAccountTagErrorExcerpt ( a , adi ) _t = ( f , l , merrcols , ex )
-- XXX findtxnerrorcolumns is awkward, I don't think this is the final form
where
( SourcePos f pos _ ) = adisourcepos adi
l = unPos pos
txt = showAccountDirective ( a , adi ) & textChomp & ( <> " \ n " )
ex = decorateTagErrorExcerpt l merrcols txt
-- Calculate columns which will help highlight the region in the excerpt
-- (but won't exactly match the real data, so won't be shown in the main error line)
merrcols = Nothing
-- don't bother for now
-- Just (col, Just col2)
-- where
-- col = undefined -- T.length (showTransactionLineFirstPart t') + 2
-- col2 = undefined -- col + T.length tagname - 1
showAccountDirective ( a , AccountDeclarationInfo { .. } ) =
" account " <> a
<> ( if not $ T . null adicomment then " ; " <> adicomment else " " )
-- | Add megaparsec-style left margin, line number, and optional column marker(s).
decorateTagErrorExcerpt :: Int -> Maybe ( Int , Maybe Int ) -> Text -> Text
decorateTagErrorExcerpt l mcols txt =
T . unlines $ ls' <> colmarkerline <> map ( lineprefix <> ) ms
where
( ls , ms ) = splitAt 1 $ T . lines txt
ls' = map ( ( T . pack ( show l ) <> " | " ) <> ) ls
colmarkerline =
[ lineprefix <> T . replicate ( col - 1 ) " " <> T . replicate regionw " ^ "
| Just ( col , mendcol ) <- [ mcols ]
, let regionw = maybe 1 ( subtract col ) mendcol + 1
]
lineprefix = T . replicate marginw " " <> " | "
where marginw = length ( show l ) + 1
_showAccountDirective = undefined
2022-05-09 11:56:47 +03:00
-- | Given a problem transaction and a function calculating the best
-- column(s) for marking the error region:
-- render it as a megaparsec-style excerpt, showing the original line number
-- on the transaction line, and a column(s) marker.
-- Returns the file path, line number, column(s) if known,
-- and the rendered excerpt, or as much of these as is possible.
2023-02-04 21:44:05 +03:00
-- The returned columns will be accurate for the rendered error message but not for the original journal data.
2022-05-09 11:56:47 +03:00
makeTransactionErrorExcerpt :: Transaction -> ( Transaction -> Maybe ( Int , Maybe Int ) ) -> ( FilePath , Int , Maybe ( Int , Maybe Int ) , Text )
makeTransactionErrorExcerpt t findtxnerrorcolumns = ( f , tl , merrcols , ex )
-- XXX findtxnerrorcolumns is awkward, I don't think this is the final form
where
( SourcePos f tpos _ ) = fst $ tsourcepos t
tl = unPos tpos
txntxt = showTransaction t & textChomp & ( <> " \ n " )
merrcols = findtxnerrorcolumns t
ex = decorateTransactionErrorExcerpt tl merrcols txntxt
-- | Add megaparsec-style left margin, line number, and optional column marker(s).
decorateTransactionErrorExcerpt :: Int -> Maybe ( Int , Maybe Int ) -> Text -> Text
decorateTransactionErrorExcerpt l mcols txt =
T . unlines $ ls' <> colmarkerline <> map ( lineprefix <> ) ms
where
( ls , ms ) = splitAt 1 $ T . lines txt
ls' = map ( ( T . pack ( show l ) <> " | " ) <> ) ls
colmarkerline =
[ lineprefix <> T . replicate ( col - 1 ) " " <> T . replicate regionw " ^ "
| Just ( col , mendcol ) <- [ mcols ]
, let regionw = maybe 1 ( subtract col ) mendcol + 1
]
lineprefix = T . replicate marginw " " <> " | "
where marginw = length ( show l ) + 1
-- | Given a problem posting and a function calculating the best
-- column(s) for marking the error region:
-- look up error info from the parent transaction, and render the transaction
-- as a megaparsec-style excerpt, showing the original line number
-- on the problem posting's line, and a column indicator.
-- Returns the file path, line number, column(s) if known,
-- and the rendered excerpt, or as much of these as is possible.
2022-07-12 16:40:49 +03:00
-- A limitation: columns will be accurate for the rendered error message but not for the original journal data.
2022-05-09 11:56:47 +03:00
makePostingErrorExcerpt :: Posting -> ( Posting -> Transaction -> Text -> Maybe ( Int , Maybe Int ) ) -> ( FilePath , Int , Maybe ( Int , Maybe Int ) , Text )
makePostingErrorExcerpt p findpostingerrorcolumns =
case ptransaction p of
Nothing -> ( " - " , 0 , Nothing , " " )
Just t -> ( f , errabsline , merrcols , ex )
where
( SourcePos f tl _ ) = fst $ tsourcepos t
2023-09-11 12:11:24 +03:00
-- p had cost removed in balanceTransactionAndCheckAssertionsB,
-- must remove them from t's postings too (#2083)
2024-01-23 20:58:26 +03:00
mpindex = transactionFindPostingIndex ( ( == p ) . postingStripCosts ) t
2023-04-27 11:40:41 +03:00
errrelline = case mpindex of
Nothing -> 0
Just pindex ->
commentExtraLines ( tcomment t ) +
sum ( map postingLines $ take pindex $ tpostings t )
where
2023-04-27 22:17:29 +03:00
-- How many lines are used to render this posting ?
2023-04-27 11:40:41 +03:00
postingLines p' = 1 + commentExtraLines ( pcomment p' )
2023-04-27 22:17:29 +03:00
-- How many extra lines does this comment add to a transaction or posting rendering ?
2023-04-27 11:40:41 +03:00
commentExtraLines c = max 0 ( length ( T . lines c ) - 1 )
2022-05-09 11:56:47 +03:00
errabsline = unPos tl + errrelline
txntxt = showTransaction t & textChomp & ( <> " \ n " )
merrcols = findpostingerrorcolumns p t txntxt
ex = decoratePostingErrorExcerpt errabsline errrelline merrcols txntxt
-- | Add megaparsec-style left margin, line number, and optional column marker(s).
decoratePostingErrorExcerpt :: Int -> Int -> Maybe ( Int , Maybe Int ) -> Text -> Text
decoratePostingErrorExcerpt absline relline mcols txt =
T . unlines $ js' <> ks' <> colmarkerline <> ms'
where
( ls , ms ) = splitAt ( relline + 1 ) $ T . lines txt
( js , ks ) = splitAt ( length ls - 1 ) ls
( js' , ks' ) = case ks of
[ k ] -> ( map ( lineprefix <> ) js , [ T . pack ( show absline ) <> " | " <> k ] )
_ -> ( [] , [] )
ms' = map ( lineprefix <> ) ms
colmarkerline =
[ lineprefix <> T . replicate ( col - 1 ) " " <> T . replicate regionw " ^ "
| Just ( col , mendcol ) <- [ mcols ]
, let regionw = 1 + maybe 0 ( subtract col ) mendcol
]
lineprefix = T . replicate marginw " " <> " | "
where marginw = length ( show absline ) + 1
-- | Find the 1-based index of the first posting in this transaction
-- satisfying the given predicate.
transactionFindPostingIndex :: ( Posting -> Bool ) -> Transaction -> Maybe Int
transactionFindPostingIndex ppredicate =
fmap fst . find ( ppredicate . snd ) . zip [ 1 .. ] . tpostings
2022-08-01 18:30:39 +03:00
-- | From the given posting, make an error excerpt showing the transaction with
-- this posting's account part highlighted.
makePostingAccountErrorExcerpt :: Posting -> ( FilePath , Int , Maybe ( Int , Maybe Int ) , Text )
makePostingAccountErrorExcerpt p = makePostingErrorExcerpt p finderrcols
where
-- Calculate columns suitable for highlighting the synthetic excerpt.
2022-08-23 13:58:31 +03:00
finderrcols p' _ _ = Just ( col , Just col2 )
2022-08-01 18:30:39 +03:00
where
2022-08-23 13:58:31 +03:00
col = 5 + if isVirtual p' then 1 else 0
col2 = col + T . length ( paccount p' ) - 1
2022-08-01 18:30:39 +03:00
2022-07-29 11:01:34 +03:00
-- | From the given posting, make an error excerpt showing the transaction with
-- the balance assertion highlighted.
makeBalanceAssertionErrorExcerpt :: Posting -> ( FilePath , Int , Maybe ( Int , Maybe Int ) , Text )
makeBalanceAssertionErrorExcerpt p = makePostingErrorExcerpt p finderrcols
where
2022-08-23 13:58:31 +03:00
finderrcols p' t trendered = Just ( col , Just col2 )
2022-07-29 11:01:34 +03:00
where
-- Analyse the rendering to find the columns to highlight.
tlines = dbg5 " tlines " $ max 1 $ length $ T . lines $ tcomment t -- transaction comment can generate extra lines
( col , col2 ) =
let def = ( 5 , maximum ( map T . length $ T . lines trendered ) ) -- fallback: underline whole posting. Shouldn't happen.
in
2022-08-23 13:58:31 +03:00
case transactionFindPostingIndex ( == p' ) t of
2022-07-29 11:01:34 +03:00
Nothing -> def
Just idx -> fromMaybe def $ do
let
beforeps = take ( idx - 1 ) $ tpostings t
beforepslines = dbg5 " beforepslines " $ sum $ map ( max 1 . length . T . lines . pcomment ) beforeps -- posting comment can generate extra lines (assume only one commodity shown)
assertionline <- dbg5 " assertionline " $ headMay $ drop ( tlines + beforepslines ) $ T . lines trendered
let
2022-08-23 13:58:31 +03:00
col2' = T . length assertionline
2022-07-29 11:01:34 +03:00
l = dropWhile ( /= '=' ) $ reverse $ T . unpack assertionline
l' = dropWhile ( ` elem ` [ '=' , '*' ] ) l
2022-08-23 13:58:31 +03:00
col' = length l' + 1
return ( col' , col2' )
2022-07-29 11:01:34 +03:00