tilia-0.1.3.0: tests/Tilia/PragmaSpec.hs
{-# LANGUAGE OverloadedStrings #-}
-- | What a module's pragmas say, read off the text.
module Tilia.PragmaSpec (spec) where
import Data.Map.Strict (Map)
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Data.EnumSet qualified as EnumSet
import GHC.Driver.Session
( Language (..),
OnOff (..),
extensionFlags,
fLangFlags,
flagSpecName,
glasgowExtsFlags,
languageExtensions,
xFlags,
)
import GHC.LanguageExtensions.Type (Extension (..))
import Test.Hspec
import Test.Hspec.QuickCheck (modifyMaxSuccess)
import Test.QuickCheck
import Tilia.Pragma
import Tilia.Pragma.Ghc (InForce (..), inForceAfter, readFlags)
spec :: Spec
spec = do
describe "movesPositions" $ do
describe "says so" $ do
for_'
[ ("a LINE pragma", "{-# LINE 1 \"Other.hs\" #-}\n"),
("a COLUMN pragma", "{-# COLUMN 20 #-}\n"),
("one written in lower case", "{-# line 1 \"Other.hs\" #-}\n"),
("one written without spaces", "{-#LINE 1 \"Other.hs\"#-}\n"),
("one written over several lines", "{-# LINE 1\n \"Other.hs\" #-}\n"),
("one below the header", "module M where\nx = 1\n{-# LINE 9 \"O.hs\" #-}\n"),
("one among pragmas that do not move anything", langThenLine),
( "one after a character literal holding a double quote",
"module M where\nq = '\"'\n{-# LINE 9 \"O.hs\" #-}\n"
)
]
(\source -> movesPositions source `shouldBe` True)
describe "says nothing of" $ do
for_'
[ ("a module with no pragma at all", "module M where\nx = 1\n"),
("a LANGUAGE pragma", "{-# LANGUAGE LambdaCase #-}\nmodule M where\n"),
("an INLINE pragma", "module M where\n{-# INLINE f #-}\nf = id\n"),
("a pragma whose name merely starts with one", "{-# LINEAR 1 #-}\n"),
("an unclosed pragma", "{-# LINE 1 \"Other.hs\"\n"),
("the word in a comment", "-- {-* LINE 1 *-}\nmodule M where\n"),
("one in a string", "module M where\nx = \"{-# LINE 1 #-}\"\n"),
( "one in a string after a character literal holding a double quote",
"module M where\nq = '\"'\nx = \"{-# LINE 1 #-}\"\n"
),
( "one in a string after a character literal holding an escaped one",
"module M where\nq = '\\\"'\nx = \"{-# LINE 1 #-}\"\n"
),
( "one in a string after a name ending in a prime",
"module M where\nx = f'\"'\"\ny = \"{-# LINE 1 #-}\"\n"
)
]
(\source -> movesPositions source `shouldBe` False)
describe "the extensions in force" $ do
it "keeps what the package puts in force" $
inForceUnder [ImplicitPrelude, GADTs] "module M where\n"
`shouldBe` [ImplicitPrelude, GADTs]
it "reads one extension" $
inForceUnder [] "{-# LANGUAGE BangPatterns #-}\nmodule M where\n"
`shouldBe` [BangPatterns]
it "reads several from one pragma" $
inForceUnder
[]
"{-# LANGUAGE LambdaCase, MagicHash #-}\nmodule M where\n"
`shouldBe` [LambdaCase, MagicHash]
it "reads several pragmas" $
inForceUnder
[]
"{-# LANGUAGE LambdaCase #-}\n{-# LANGUAGE MagicHash #-}\n"
`shouldBe` [LambdaCase, MagicHash]
it "ignores flags that turn on no extension" $
inForceUnder
[]
"{-# OPTIONS_GHC -Wall #-}\n{-# LANGUAGE LambdaCase #-}\n"
`shouldBe` [LambdaCase]
it "reads the -X flags of OPTIONS_GHC" $
inForceUnder
[GADTs]
"{-# OPTIONS_GHC -Wall -XLambdaCase -XNoGADTs #-}\nmodule M where\n"
`shouldBe` [LambdaCase]
it "reads the flags of OPTIONS" $
inForceUnder [] "{-# OPTIONS -XLambdaCase #-}\n"
`shouldBe` [LambdaCase]
it "reads the flags of an options pragma as GHC splits them" $
inForceUnder [] "{-# OPTIONS_GHC \"-XLambdaCase\" #-}\n"
`shouldBe` [LambdaCase]
it "reads -cpp as turning on CPP" $
inForceUnder [] "{-# OPTIONS_GHC -cpp #-}\n"
`shouldBe` [Cpp]
it "reads the old -f spellings of extensions" $
inForceUnder
[ImplicitPrelude]
"{-# OPTIONS_GHC -fbang-patterns -fno-implicit-prelude #-}\n"
`shouldBe` [BangPatterns]
it "reads -fglasgow-exts as turning on what it stands for" $
inForceUnder [] "{-# OPTIONS_GHC -fglasgow-exts #-}\n"
`shouldSatisfy` \es -> all (`elem` es) glasgowExtsFlags
it "reads -fno-glasgow-exts as turning that off again" $
inForceUnder
[]
"{-# OPTIONS_GHC -fglasgow-exts -fno-glasgow-exts #-}\n"
`shouldSatisfy` not . any (`elem` glasgowExtsFlags)
it "reads the pragmas in the order they are written" $
inForceUnder
[]
( "{-# LANGUAGE LambdaCase #-}\n"
<> "{-# OPTIONS_GHC -XNoLambdaCase -XMagicHash #-}\n"
<> "{-# LANGUAGE NoMagicHash #-}\n"
)
`shouldBe` []
it "reads the names of a language pragma past the comments in it" $
inForceUnder
[]
"{-# LANGUAGE\n LambdaCase -- for f\n , {- c -} MagicHash\n #-}\nmodule M where\n"
`shouldBe` [LambdaCase, MagicHash]
it "ignores a pragma below the module line, which GHC does not read" $
inForceUnder [] "module M where\n{-# LANGUAGE LambdaCase #-}\n"
`shouldBe` []
it "reads the pragmas between the directives of a header" $
inForceUnder
[]
"{-# LANGUAGE CPP #-}\n#if 1\n{-# LANGUAGE LambdaCase #-}\n#endif\nmodule M where\n"
`shouldBe` [Cpp, LambdaCase]
it "takes an edition the module names in place of the component's" $
inForceUnder
(languageExtensions (Just Haskell2010))
"{-# LANGUAGE GHC2024 #-}\nmodule M where\n"
`shouldBe` languageExtensions (Just GHC2024)
it "keeps what the component switches on top of the module's edition" $
effectiveExtensions
(defaultsOf (languageExtensions (Just Haskell2010)) [On OverloadedStrings])
"{-# LANGUAGE GHC2021 #-}\nmodule M where\n"
`shouldSatisfy` \es -> all (`elem` es) [OverloadedStrings, ImportQualifiedPost]
it "switches what the module switches on top of its edition, wherever named" $
inForceUnder
[]
"{-# LANGUAGE NoLambdaCase #-}\n{-# LANGUAGE GHC2024 #-}\nmodule M where\n"
`shouldSatisfy` notElem LambdaCase
it "takes the edition named last" $
inForceUnder
[]
"{-# LANGUAGE GHC2024 #-}\n{-# LANGUAGE Haskell2010 #-}\nmodule M where\n"
`shouldBe` languageExtensions (Just Haskell2010)
it "reads an edition from a -X flag" $
inForceUnder [] "{-# OPTIONS_GHC -XGHC2024 #-}\nmodule M where\n"
`shouldBe` languageExtensions (Just GHC2024)
it "ignores the options of other compilers" $
inForceUnder [] "{-# OPTIONS_JHC -fffi -cpp #-}\n"
`shouldBe` []
it "ignores an unknown extension rather than failing" $
inForceUnder [] "{-# LANGUAGE LambdaCase, NotARealExtension #-}\n"
`shouldBe` [LambdaCase]
it "treats a No-prefix as turning one off" $
inForceUnder
[ImplicitPrelude]
"{-# LANGUAGE NoImplicitPrelude #-}\n"
`shouldBe` []
it "lets a module refuse what its package put in force" $
inForceUnder [ImplicitPrelude, GADTs] "{-# LANGUAGE NoGADTs #-}\n"
`shouldBe` [ImplicitPrelude]
it "lets a module take back a Prelude its package turned off" $
inForceUnder [GADTs] "{-# LANGUAGE ImplicitPrelude #-}\n"
`shouldBe` [GADTs, ImplicitPrelude]
it "does not repeat an extension named twice" $
inForceUnder
[]
"{-# LANGUAGE LambdaCase #-}\n{-# LANGUAGE LambdaCase #-}\n"
`shouldBe` [LambdaCase]
it "turns on what an extension implies, as GHC does" $
inForceUnder [] "{-# LANGUAGE RankNTypes #-}\n"
`shouldBe` [RankNTypes, ExplicitForAll]
it "turns off what an extension implies is off" $
inForceUnder
[ImplicitPrelude]
"{-# LANGUAGE RebindableSyntax #-}\n"
`shouldBe` [RebindableSyntax]
it "leaves on what an extension turned off had implied" $
inForceUnder [] "{-# LANGUAGE RankNTypes, NoRankNTypes #-}\n"
`shouldBe` [ExplicitForAll]
it "lets a later name turn off what an earlier one implied" $
inForceUnder [] "{-# LANGUAGE RankNTypes, NoExplicitForAll #-}\n"
`shouldBe` [RankNTypes]
modifyMaxSuccess (const 5000) . describe "against GHC's own flag reader" $ do
it "leaves on what GHC leaves on" $
forAll (listOf1 (pragmaOf headerNames)) $ \pragmas ->
let ours =
effectiveExtensions
(defaultsOf (languageExtensions Nothing) [])
(headerOf pragmas)
theirs =
EnumSet.toList (extensionFlags (readFlags (concatMap flagsOf pragmas)))
in counterexample (T.unpack (headerOf pragmas)) $
Set.fromList ours === Set.fromList theirs
it "takes two orders to be alike where GHC switches alike" $
forAll (listOf1 (pragmaOf extensionNames)) $ \pragmas ->
forAll (shuffle pragmas) $ \shuffled ->
counterexample (T.unpack (headerOf pragmas <> headerOf shuffled)) $
sameInForce (fmap namedBody pragmas) (fmap namedBody shuffled)
=== (ghcSwitches pragmas == ghcSwitches shuffled)
describe "lookupExtension" $ do
it "knows an extension by the name one writes" $
lookupExtension "LambdaCase" `shouldBe` Just LambdaCase
it "says nothing of a name no compiler knows" $
lookupExtension "NotARealExtension" `shouldBe` Nothing
it "does not accept the No-prefixed spelling as a name" $
lookupExtension "NoImplicitPrelude" `shouldBe` Nothing
where
for_' cases expect = mapM_ (\(what, source) -> it what (expect source)) cases
langThenLine :: Text
langThenLine = "{-# LANGUAGE LambdaCase #-}\nmodule M where\n{-# LINE 3 \"O.hs\" #-}\n"
-- | The extensions in force in a module of a component whose edition puts
-- these in force and which switches nothing itself.
inForceUnder :: [Extension] -> Text -> [Extension]
inForceUnder edition = effectiveExtensions (defaultsOf edition [])
-- | A pragma of a header, as its name and its body, with the flags it hands
-- GHC.
type HeaderPragma = (Text, Text, [Text])
-- | A pragma that names one of these, or spells an extension as an old flag.
pragmaOf :: [Text] -> Gen HeaderPragma
pragmaOf names = oneof [language, options]
where
language = do
name <- elements names
pure ("LANGUAGE", name, ["-X" <> name])
options = do
flag <- elements oldFlags
pure ("OPTIONS_GHC", flag, [flag])
oldFlags =
["-cpp", "-fglasgow-exts", "-fno-glasgow-exts"]
<> concat
[ ["-f" <> name, "-fno-" <> name]
| name <- T.pack . flagSpecName <$> fLangFlags
]
-- | Every name of an extension, with @No@ in front and without.
extensionNames :: [Text]
extensionNames = names <> fmap ("No" <>) names
where
names = T.pack . flagSpecName <$> xFlags
-- | Every name a @LANGUAGE@ pragma can hold: the extensions and the editions.
headerNames :: [Text]
headerNames =
extensionNames <> [T.pack (show l) | l <- [minBound .. maxBound :: Language]]
-- | A module of nothing but a header of these pragmas.
headerOf :: [HeaderPragma] -> Text
headerOf pragmas =
T.concat ["{-# " <> n <> " " <> b <> " #-}\n" | (n, b, _) <- pragmas]
<> "module M where\n"
flagsOf :: HeaderPragma -> [Text]
flagsOf (_, _, flags) = flags
namedBody :: HeaderPragma -> (Text, Text)
namedBody (n, b, _) = (n, b)
-- | How GHC takes these pragmas to switch each extension.
ghcSwitches :: [HeaderPragma] -> Map Extension Bool
ghcSwitches = inForceSwitches . inForceAfter . concatMap flagsOf