tadka-2.0.0.0: test/interop/Main.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
-- | Round-trip interop tests: for each supported library, construct a known
-- failure position, convert it, resolve against the same source, and confirm
-- the line/column matches what the library itself reports (attoparsec, which
-- reports no line/column, is checked on consumed-offset instead).
module Main (main) where
import Control.Monad (unless)
import Data.Text (Text)
import Data.Void (Void)
import System.Exit (exitFailure)
import qualified Data.Attoparsec.Text as A
import qualified GHC.Data.Strict as Strict
import GHC.Data.FastString (fsLit)
import GHC.Types.SrcLoc (SrcSpan (..), mkRealSrcLoc, mkRealSrcSpan,
srcSpanEndCol, srcSpanEndLine, srcSpanStartCol,
srcSpanStartLine)
import qualified Data.List.NonEmpty as NE
import Text.Megaparsec (Parsec, attachSourcePos, bundleErrors,
bundlePosState, runParser)
import Text.Megaparsec.Char (char, string)
import Text.Megaparsec.Error (errorOffset)
import Text.Megaparsec.Pos (sourceColumn, sourceLine, unPos)
import Tadka
import Tadka.Interop.Attoparsec (consumedOffset)
import Tadka.Interop.GHC (SrcSpanConvError (..), spanFromSrcSpan)
import Tadka.Interop.Megaparsec (spanFromError)
rightOrErr :: Show a => Either a b -> b
rightOrErr = either (error . show) id
resolveAt :: NamedSource -> Span -> (Int, Int)
resolveAt ns sp =
let rs = rightOrErr (resolveSpan ns sp)
lc = resolvedStart rs
in (lcLine lc, lcColumn lc)
-- === GHC ===================================================================
ghcCheck :: (String, Bool)
ghcCheck =
let src = "aaaa\nbbbbbb\ncccc\n"
ns = rightOrErr (mkNamedSource "f.hs" src)
rss = mkRealSrcSpan (mkRealSrcLoc (fsLit "f.hs") 2 3) (mkRealSrcLoc (fsLit "f.hs") 2 6)
ss = RealSrcSpan rss Strict.Nothing
sp = rightOrErr (spanFromSrcSpan src ss)
mine = resolveAt ns sp
ghc = (srcSpanStartLine rss, srcSpanStartCol rss)
in ("ghc SrcSpan line/col round-trips", mine == ghc && mine == (2, 3))
-- A span whose start resolves but whose end is out of bounds must name the
-- END position in the error, not fall back to reporting the start.
ghcEndAttributionCheck :: (String, Bool)
ghcEndAttributionCheck =
let src = "aaaa\nbbbbbb\ncccc\n"
rss = mkRealSrcSpan (mkRealSrcLoc (fsLit "f.hs") 2 3) (mkRealSrcLoc (fsLit "f.hs") 99 1)
ss = RealSrcSpan rss Strict.Nothing
in ( "ghc SrcSpan out-of-bounds end is attributed to the end, not the start"
, spanFromSrcSpan src ss == Left (LineColOutOfBounds (srcSpanEndLine rss) (srcSpanEndCol rss))
)
-- === megaparsec ============================================================
type P = Parsec Void Text
megaCheck :: (String, Bool)
megaCheck =
let src = "abc\ndef"
ns = rightOrErr (mkNamedSource "f.hs" src)
p = string "abc" *> char '\n' *> string "xyz" :: P Text
in case runParser p "f.hs" src of
Right _ -> ("megaparsec offset line/col round-trips", False)
Left bundle ->
let err = NE.head (bundleErrors bundle)
(withPos,_) = attachSourcePos errorOffset (bundleErrors bundle) (bundlePosState bundle)
sp' = snd (NE.head withPos)
mega = (unPos (sourceLine sp'), unPos (sourceColumn sp'))
sp = rightOrErr (spanFromError 0 err)
mine = resolveAt ns sp
in ("megaparsec offset line/col round-trips", mine == mega && mine == (2, 1))
-- === attoparsec ============================================================
attoCheck :: (String, Bool)
attoCheck =
let src = "hello\nworld"
ns = rightOrErr (mkNamedSource "f.hs" src)
p = A.string "hello" *> A.char '\n' *> A.string "xxx"
res = A.feed (A.parse p src) ""
in case res of
A.Fail remaining _ _ ->
let off = consumedOffset src remaining
sp = rightOrErr (mkSpan off 0)
mine = resolveAt ns sp
in ("attoparsec consumed offset resolves", off == 6 && mine == (2, 1))
_ -> ("attoparsec consumed offset resolves", False)
main :: IO ()
main = do
let checks = [ghcCheck, ghcEndAttributionCheck, megaCheck, attoCheck]
results <- mapM report checks
unless (and results) exitFailure
where
report (name, ok) = do
putStrLn ((if ok then " ok " else " FAIL ") <> name)
pure ok