packages feed

tilia-0.1.0.0: tests/Tilia/Fixity/InterfaceSpec.hs

{-# LANGUAGE OverloadedStrings #-}

-- | Reading what @ghc --show-iface@ prints.
--
-- The samples below are cut from real output rather than invented, since
-- the whole risk here is in the format: this is a pretty-printer's idea of
-- an interface, not a documented one.
module Tilia.Fixity.InterfaceSpec (spec) where

import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Data.Text (Text)
import Test.Hspec
import Tilia.Fixity
import Tilia.Fixity.HiFile (HiExport (..), HiFile (..), HiName (..))
import Tilia.Fixity.Interface

spec :: Spec
spec = do
  describe "what a module declares" $ do
    it "reads a fixity line" $
      declares "fixities infixl 9 !, infixl 9 !?, infixl 9 \\\\\n"
        `shouldBe` [ (OpName "!", Fixity LeftAssoc 9),
                     (OpName "!?", Fixity LeftAssoc 9),
                     (OpName "\\\\", Fixity LeftAssoc 9)
                   ]

    it "reads one wrapped across lines" $
      declares
        "fixities infixr 0 $, infixr 0 $!, infixl 4 *>, infixr 5 ++,\n\
        \         infixr 9 ., infixr 5 :|, infixl 4 <$\n"
        `shouldBe` [ (OpName "$", Fixity RightAssoc 0),
                     (OpName "$!", Fixity RightAssoc 0),
                     (OpName "*>", Fixity LeftAssoc 4),
                     (OpName "++", Fixity RightAssoc 5),
                     (OpName ".", Fixity RightAssoc 9),
                     (OpName ":|", Fixity RightAssoc 5),
                     (OpName "<$", Fixity LeftAssoc 4)
                   ]

    it "reads every direction, and a name used in backticks" $
      declares "fixities infixl 7 div, infix 4 ===, infixr 1 .&&.\n"
        `shouldBe` [ (OpName ".&&.", Fixity RightAssoc 1),
                     (OpName "===", Fixity NoAssoc 4),
                     (OpName "div", Fixity LeftAssoc 7)
                   ]

    it "reads the precedence GHC gives the function arrow" $
      declares "fixities infixr -1 ->\n"
        `shouldBe` [(OpName "->", Fixity RightAssoc (-1))]

    it "reads it alongside ordinary ones" $
      declares "fixities infixr -1 ->, infixl 9 !\n"
        `shouldBe` [(OpName "!", Fixity LeftAssoc 9), (OpName "->", Fixity RightAssoc (-1))]

    it "passes over an entry it cannot read, and keeps the rest" $
      declares "fixities infixl notadigit ?, infixl 9 !, infixl\n"
        `shouldBe` [(OpName "!", Fixity LeftAssoc 9)]

    it "says nothing for a module that declares nothing" $
      declares "exports:\n  member\n" `shouldBe` []

  describe "what a module passes on" $ do
    it "names the module an operator was declared in" $
      passesOn "exports:\n  Data.Aeson.Types.FromJSON..:\n"
        `shouldBe` [("Data.Aeson.Types.FromJSON", OpName ".:")]

    it "leaves out what the module declared itself" $
      passesOn "exports:\n  decode'\n  <+>\n" `shouldBe` []

    it "takes the members of a class along with it" $
      passesOn
        "exports:\n\
        \  Data.Aeson.Types.FromJSON.FromJSON{Data.Aeson.Types.FromJSON.parseJSON}\n"
        `shouldBe` [ ("Data.Aeson.Types.FromJSON", OpName "FromJSON"),
                     ("Data.Aeson.Types.FromJSON", OpName "parseJSON")
                   ]

    it "takes a record field, written after a bar" $
      passesOn "exports:\n  Data.Aeson.Encoding.Internal.Encoding'|{Data.Aeson.Encoding.Internal.fromEncoding}\n"
        `shouldBe` [ ("Data.Aeson.Encoding.Internal", OpName "Encoding'"),
                     ("Data.Aeson.Encoding.Internal", OpName "fromEncoding")
                   ]

    it "keeps a type apart from the module holding it" $
      passesOn "exports:\n  Data.Aeson.Types.Internal.Value\n"
        `shouldBe` [("Data.Aeson.Types.Internal", OpName "Value")]

    it "reads an operator that is nothing but a dot" $
      passesOn "exports:\n  Data.Function..\n"
        `shouldBe` [("Data.Function", OpName ".")]

    it "leaves a capitalised name this module declared alone" $
      passesOn "exports:\n  Value\n" `shouldBe` []

  describe "operators with a vertical bar in them" $ do
    -- A bar also marks a type exported without itself, as in T|{...}, so
    -- these are read in the same text as that mark.
    it "reads one made of bars alone" $
      passesOn "exports:\n  GHC.Internal.Data.Type.Bool.||\n"
        `shouldBe` [("GHC.Internal.Data.Type.Bool", OpName "||")]

    it "reads them among the members of a type exported without itself" $
      passesOn "exports:\n  GHC.Internal.Bits.Bits|{GHC.Internal.Bits..&. GHC.Internal.Bits..|. GHC.Internal.Bits.complement}\n"
        `shouldBe` [ ("GHC.Internal.Bits", OpName "Bits"),
                     ("GHC.Internal.Bits", OpName ".&."),
                     ("GHC.Internal.Bits", OpName ".|."),
                     ("GHC.Internal.Bits", OpName "complement")
                   ]

    it "takes them as the members of such a type" $
      members "exports:\n  GHC.Internal.Base.Alternative|{GHC.Internal.Base.<|> GHC.Internal.Base.empty}\n"
        `shouldBe` [(OpName "Alternative", [OpName "<|>", OpName "empty"])]

  describe "which namespace a fixity governs" $ do
    it "gives one to types where the module declares a type of that name" $
      declaresIn "fixities infix 4 :~:\nab12\n  data (:~:) a b where\n"
        `shouldBe` [((InTypes, OpName ":~:"), Fixity NoAssoc 4)]

    it "gives one to terms where nothing declares a type of that name" $
      declaresIn "fixities infixl 9 !\nab12\n  (!) :: Int -> Int -> Int\n"
        `shouldBe` [((InTerms, OpName "!"), Fixity LeftAssoc 9)]

    it "reads a type synonym as a type" $
      declaresIn "fixities infixr 5 :+\nab12\n  type (:+) :: * -> * -> *\n"
        `shouldBe` [((InTypes, OpName ":+"), Fixity RightAssoc 5)]

    it "reads a type family as a type" $
      declaresIn "fixities infixl 6 ==\nab12\n  type family (==) a b where\n"
        `shouldBe` [((InTypes, OpName "=="), Fixity LeftAssoc 6)]

    it "reads a class as a type" $
      declaresIn "fixities infixl 4 <%>\nab12\n  class (<%>) a where\n"
        `shouldBe` [((InTypes, OpName "<%>"), Fixity LeftAssoc 4)]

    it "takes a role declaration as saying the name is a type" $
      declaresIn "fixities infixl 9 !\nab12\n  type role (!) nominal\n"
        `shouldBe` [((InTypes, OpName "!"), Fixity LeftAssoc 9)]

    it "is not misled by declarations of other names" $
      declaresIn "fixities infixl 9 !\nab12\n  data Other a b where\n  (!) :: Int\n"
        `shouldBe` [((InTerms, OpName "!"), Fixity LeftAssoc 9)]

  describe "the members of a name" $ do
    it "takes the members an entry wears in braces" $
      members "exports:\n  GHC.Internal.Base.NonEmpty{GHC.Internal.Base.:|}\n"
        `shouldBe` [(OpName "NonEmpty", [OpName ":|"])]

    it "takes every one of them" $
      members
        "exports:\n\
        \  GHC.Internal.Base.Applicative{GHC.Internal.Base.*> GHC.Internal.Base.<*> GHC.Internal.Base.pure}\n"
        `shouldBe` [(OpName "Applicative", [OpName "*>", OpName "<*>", OpName "pure"])]

    it "takes them from a partial export, which still says what it has" $
      members "exports:\n  GHC.Internal.Base.Functor|{GHC.Internal.Base.<$}\n"
        `shouldBe` [(OpName "Functor", [OpName "<$"])]

    it "takes a name this module declared, written without a module" $
      members "exports:\n  WrappedArrow{WrapArrow unwrapArrow}\n"
        `shouldBe` [(OpName "WrappedArrow", [OpName "WrapArrow", OpName "unwrapArrow"])]

    it "keeps entries apart where several sit on one line" $
      members "exports:\n  A{B} C{D}\n"
        `shouldBe` [(OpName "A", [OpName "B"]), (OpName "C", [OpName "D"])]

    it "has nothing to say about a name without members" $
      members "exports:\n  decode'\n  Data.Aeson.Types.FromJSON..:\n" `shouldBe` []

  describe "what a decoded interface brings in" $ do
    it "is every name it exports, by namespace, and the members of each type" $
      fmap
        interfaceExports
        (fromHiFile "M" (HiFile "M" [AvailTC (inM InTypes "T") [inM InTypes "T", inM InTerms "C"], Avail (inM InTerms "f")] []))
        `shouldBe` Right
          ( Just
              ( Certain
                  (Set.fromList [(InTypes, OpName "T"), (InTerms, OpName "C"), (InTerms, OpName "f")])
                  (Map.fromList [(OpName "T", Set.fromList [(InTerms, OpName "C")])])
              )
          )

    it "does not take a type for exported where only its field is" $
      fmap
        (fmap certainNames . interfaceExports)
        (fromHiFile "M" (HiFile "M" [AvailTC (inM InTypes "T") [inM InTerms "field"]] []))
        `shouldBe` Right (Just (Set.fromList [(InTerms, OpName "field")]))

  describe "sections it has no use for" $
    it "is not confused by the rest of the file" $ do
      let out =
            "Magic: Wanted 33214052,\n\
            \       got    33214052\n\
            \interface Data.Aeson 9103\n\
            \  interface hash: 6b4f\n\
            \exports:\n\
            \  Data.Aeson.Types.FromJSON..:\n\
            \fixities infixl 9 !\n\
            \direct package dependencies: base-4.20.2.0 bytestring-0.12.2.0\n\
            \orphans: Data.Orphans\n\
            \trusted: none\n"
      fmap (Map.toList . interfaceDeclares) (parseInterface "Data.Aeson" out)
        `shouldBe` Just [((InTerms, OpName "!"), Fixity LeftAssoc 9)]
      fmap interfaceReexports (parseInterface "Data.Aeson" out)
        `shouldBe` Just [("Data.Aeson.Types.FromJSON", OpName ".:")]

  describe "output it will not read" $ do
    it "refuses what does not name a module at all" $
      parseInterface "M" "some future rendering we do not recognise\n"
        `shouldBe` Nothing

    it "refuses an interface for a different module" $
      parseInterface "Data.Map.Strict" (header "Data.Map.Lazy" <> "fixities infixl 9 !\n")
        `shouldBe` Nothing

    it "reads one that names the module asked for" $
      declares "fixities infixl 9 !\n" `shouldBe` [(OpName "!", Fixity LeftAssoc 9)]

header :: Text -> Text
header modName = "interface " <> modName <> " 9103\n"

-- | The fixities an interface of this shape declares, by name alone.
declares :: Text -> [(OpName, Fixity)]
declares =
  fmap (\((_, op), fixity) -> (op, fixity))
    . maybe [] (Map.toList . interfaceDeclares)
    . parseInterface "M"
    . (header "M" <>)

-- | The same, keeping the namespace each governs.
declaresIn :: Text -> [((Namespace, OpName), Fixity)]
declaresIn =
  maybe [] (Map.toList . interfaceDeclares) . parseInterface "M" . (header "M" <>)

passesOn :: Text -> [(Text, OpName)]
passesOn = maybe [] interfaceReexports . parseInterface "M" . (header "M" <>)

-- | The members of each exported name, in a settled order.
members :: Text -> [(OpName, [OpName])]
members =
  maybe [] (fmap (fmap Set.toList) . Map.toList . interfaceMembers)
    . parseInterface "M"
    . (header "M" <>)

-- | A name the module @M@ declares.
inM :: Namespace -> Text -> HiName
inM namespace = HiName "M" namespace . OpName