packages feed

tadka-2.0.0.0: test/golden/Fixtures.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}

-- | Concrete diagnostics for the graphical golden fixtures (spec Phase 5).
-- Offsets are computed from prefix lengths so the labelled spans are exact.
module Fixtures
  ( fixtures
  , narratableFixtures
  , jsonFixtures
  , contextFixtures
  , generatedSource
  , single
  ) where

import           Data.List.NonEmpty (NonEmpty (..))
import           Data.Text     (Text)
import qualified Data.Text     as T
import           Prettyprinter (Doc, pretty)
import           Language.Haskell.TH (litE, pprint, stringL)

import           Tadka
import           Tadka.Internal (buildContext, buildContextMulti, buildContextWith)

-- A general-purpose Diagnostic carrier for fixtures.
data Fix = Fix
  { fMsg  :: Text
  , fCode :: Maybe DiagnosticCode
  , fCtx  :: Context
  , fHelp :: Maybe Text
  , fUrl  :: Maybe Url
  , fRel  :: [SomeDiagnostic]
  , fId   :: Maybe DiagnosticId
  }

instance Diagnostic Fix where
  message      = pretty . fMsg
  code         = fCode
  context      = fCtx
  help         = fmap pretty . fHelp
  url          = fUrl
  related      = fRel
  diagnosticId = fId

rightOrErr :: Show a => Either a b -> b
rightOrErr = either (error . show) id

mkCode :: Text -> DiagnosticCode
mkCode = rightOrErr . mkDiagnosticCode

label :: Text -> Maybe (Doc Ann)
label t = Just (pretty t)

-- 1. Single label -----------------------------------------------------------
src1 :: NamedSource
src1 = rightOrErr (mkNamedSource "example.hs" "line1\nline2\nlet x = foo + 1\n")

-- A span that crosses lines, to exercise the connector lanes (Phase III).
multiLine :: Fix
multiLine = Fix
  { fMsg  = "unterminated function call"
  , fCode = Just (mkCode "tadka::E0300")
  , fCtx  = buildContext srcCall
              [ (rightOrErr (mkSpan 8 22), label "opened here, never closed") ]
  , fHelp = Just "add a closing `)`"
  , fUrl  = Nothing, fRel = [], fId = Nothing
  }

srcCall :: NamedSource
srcCall = rightOrErr (mkNamedSource "call.hs" "let x = foo(\n    bar,\n    baz)\n")

-- Labels far apart, to exercise context lines + gap elision (rendered with
-- withContextLines 1 in the golden harness).
farApart :: Fix
farApart = Fix
  { fMsg  = "two distant problems"
  , fCode = Just (mkCode "tadka::E0200")
  , fCtx  = buildContextWith srcTen
              [ (rightOrErr (mkSpan 4 2),  Secondary, label "first")
              , (rightOrErr (mkSpan 40 4), Primary,   label "second")
              ]
  , fHelp = Nothing, fUrl = Nothing, fRel = [], fId = Nothing
  }

srcTen :: NamedSource
srcTen = rightOrErr (mkNamedSource "big.hs"
  "one aaa\ntwo bbb\nthree c\nfour dd\nfive ee\nsix fff\nseven g\neight h\nnine ii\nten jjj\n")

-- A minimal chainable diagnostic for exercising the cause chain (Part B).
data CauseNode = CauseNode Text (Maybe DiagnosticCode) (Maybe SomeDiagnostic)

instance Diagnostic CauseNode where
  message (CauseNode m _ _) = pretty m
  code    (CauseNode _ c _) = c
  diagnosticCause (CauseNode _ _ mc) = mc

-- E0100 caused by E0042 caused by an uncoded leaf.
withCause :: CauseNode
withCause = CauseNode "failed to compile `Main`" (Just (mkCode "tadka::E0100"))
  (Just (SomeDiagnostic (CauseNode "type mismatch in `foo`" (Just (mkCode "tadka::E0042"))
    (Just (SomeDiagnostic (CauseNode "unbound variable `x`" Nothing Nothing))))))

single :: Fix
single = Fix
  { fMsg  = "undefined variable `foo`"
  , fCode = Just (mkCode "tadka::E0001")
  , fCtx  = buildContext src1
              [ (rightOrErr (mkSpan (T.length "line1\nline2\nlet x = ") 3), label "not in scope") ]
  , fHelp = Just "did you mean `bar`?"
  , fUrl  = Just (rightOrErr (mkUrl "https://example.org/errors/E0001"))
  , fRel  = []
  , fId   = Nothing
  }

-- 2. Multiple labels + related ----------------------------------------------
src2 :: NamedSource
src2 = rightOrErr (mkNamedSource "example.hs"
         "addOne :: Int -> Int\naddOne x = x\nresult = addOne \"hi\"\n")

related43 :: Fix
related43 = Fix
  { fMsg  = "conflicting instance defined here"
  , fCode = Just (mkCode "tadka::E0043")
  , fCtx  = buildContext (rightOrErr (mkNamedSource "Prelude.hs" "instance Num String where ...\n"))
              [ (rightOrErr (mkSpan 0 8), label "conflicting instance") ]
  , fHelp = Nothing, fUrl = Nothing, fRel = [], fId = Nothing
  }

multi :: Fix
multi = Fix
  { fMsg  = "type mismatch"
  , fCode = Just (mkCode "tadka::E0042")
  , fCtx  = buildContextWith src2
              [ (rightOrErr (mkSpan (T.length "addOne :: ") 3), Secondary, label "expected because of this")
              , (rightOrErr (mkSpan (T.length "addOne :: Int -> Int\naddOne x = x\nresult = addOne ") 4),
                 Primary, label "found `String`, expected `Int`")
              ]
  , fHelp = Just "convert with `show` or change the annotation"
  , fUrl  = Nothing
  , fRel  = [SomeDiagnostic related43]
  , fId   = Nothing
  }

-- 3. Degraded (stale) label -------------------------------------------------
degraded :: Fix
degraded = Fix
  { fMsg  = "undefined variable `foo`"
  , fCode = Just (mkCode "tadka::E0001")
  , fCtx  = buildContext (rightOrErr (mkNamedSource "example.hs" "let x = 1\n"))
              [ (rightOrErr (mkSpan 100 3), label "not in scope") ]  -- out of bounds -> stale
  , fHelp = Just "did you mean `bar`?"
  , fUrl  = Nothing, fRel = [], fId = Nothing
  }

-- 4. Cycle-omitted related --------------------------------------------------
cyc :: Fix
cyc =
  let d = Fix
            { fMsg  = "conflicting instance defined here"
            , fCode = Just (mkCode "tadka::E0043")
            , fCtx  = buildContext (rightOrErr (mkNamedSource "Prelude.hs" "instance Num String where ...\n"))
                        [ (rightOrErr (mkSpan 0 8), label "conflicting instance") ]
            , fHelp = Nothing, fUrl = Nothing
            , fRel  = [SomeDiagnostic d]                 -- self-reference => cycle
            , fId   = Just (mkDiagnosticId "e0043")
            }
  in d

-- 5. Cross-file (Phase 12) -----------------------------------------------
-- Two sources, each contributing one label: the shape a real "this import
-- disagrees with that definition" diagnostic takes once a Context can span
-- more than one file. Exercises all three renderers via the shared fixture
-- lists below, same as 'single'/'withCause' already do.
srcModA :: NamedSource
srcModA = rightOrErr (mkNamedSource "ModuleA.hs" "import ModuleB (widget)\nmain = widget\n")

srcModB :: NamedSource
srcModB = rightOrErr (mkNamedSource "ModuleB.hs" "widget :: Int\nwidget = 42\n")

crossFile :: Fix
crossFile = Fix
  { fMsg  = "type mismatch across modules"
  , fCode = Just (mkCode "tadka::E0500")
  , fCtx  = buildContextMulti
              ( ( srcModA
                , [ (rightOrErr (mkSpan (T.length "import ModuleB (") 6), Secondary,
                     label "imported here")
                  ]
                )
              :| [ ( srcModB
                   , [ (rightOrErr (mkSpan 0 6), Primary, label "defined here as `Int`") ]
                   )
                 ]
              )
  , fHelp = Just "the two declarations must agree"
  , fUrl  = Nothing, fRel = [], fId = Nothing
  }

fixtures :: [(String, SomeDiagnostic)]
fixtures =
  [ ("single-label",  SomeDiagnostic single)
  , ("multi-label",   SomeDiagnostic multi)
  , ("degraded",      SomeDiagnostic degraded)
  , ("cycle-omitted", SomeDiagnostic cyc)
  , ("tab-indented",  SomeDiagnostic tabIndented)
  , ("with-cause",    SomeDiagnostic withCause)
  , ("multi-line",     SomeDiagnostic multiLine)
  , ("cross-file",     SomeDiagnostic crossFile)
  ]

-- Tab-indented source: the caret must align under the tab-EXPANDED position.
tabIndented :: Fix
tabIndented = Fix
  { fMsg  = "undefined variable `foo`"
  , fCode = Just (mkCode "tadka::E0001")
  , fCtx  = buildContext
              (rightOrErr (mkNamedSource "tab.hs" "func x =\n\t  return foo\n"))
              [ (rightOrErr (mkSpan (T.length "func x =\n\t  return ") 3), label "not in scope") ]
  , fHelp = Nothing, fUrl = Nothing, fRel = [], fId = Nothing
  }

-- Narratable fixtures ------------------------------------------------------
-- A related chain deeper than the depth limit the golden runner renders at,
-- so the prose truncation marker fires.
relB :: Fix
relB = Fix "second related problem" (Just (mkCode "pkg::E1002"))
           NoContext Nothing Nothing [] Nothing

relA :: Fix
relA = Fix "first related problem" (Just (mkCode "pkg::E1001"))
           NoContext Nothing Nothing [SomeDiagnostic relB] Nothing

truncatedRoot :: Fix
truncatedRoot = Fix
  { fMsg  = "top-level problem"
  , fCode = Just (mkCode "pkg::E1000")
  , fCtx  = buildContext src1
              [ (rightOrErr (mkSpan (T.length "line1\nline2\nlet x = ") 3), label "here") ]
  , fHelp = Just "see the related items"
  , fUrl  = Nothing
  , fRel  = [SomeDiagnostic relA]
  , fId   = Nothing
  }

narratableFixtures :: [(String, SomeDiagnostic)]
narratableFixtures =
  [ ("narr-single",    SomeDiagnostic single)
  , ("narr-truncated", SomeDiagnostic truncatedRoot)
  , ("narr-cause",     SomeDiagnostic withCause)
  , ("narr-cross-file", SomeDiagnostic crossFile)
  ]

-- JSON fixtures (rendered at depth limit 1 by the runner): single (matches the
-- vision example), cycle (cycleOmitted flag), truncated (nested truncated flag).
contextFixtures :: [(String, SomeDiagnostic)]
contextFixtures =
  [ ("context-elision", SomeDiagnostic farApart) ]

jsonFixtures :: [(String, SomeDiagnostic)]
jsonFixtures =
  [ ("json-single",    SomeDiagnostic single)
  , ("json-cycle",     SomeDiagnostic cyc)
  , ("json-truncated", SomeDiagnostic truncatedRoot)
  , ("json-cause",     SomeDiagnostic withCause)
  , ("json-cross-file", SomeDiagnostic crossFile)
  ]


data ParseError = UnexpectedToken
  { errSource :: NamedSource
  , got       :: Text
  , expected  :: Text
  , at        :: Span
  }
  deriving (Show)

$(do
    decs <- deriveDiagnostic defaultSpec
              { specCode        = Just "tadka::E0001"
              , specHelp        = Just "did you forget a semicolon?"
              , specSourceField = Just 'errSource
              , specLabelFields = [('at, "unexpected token here")]
              }
              ''ParseError
    dump <- [d| generatedSource :: String
                generatedSource = $(litE (stringL (pprint decs))) |]
    pure (decs ++ dump))