tilia-0.1.3.0: tests/Tilia/EquivalenceSpec.hs
{-# LANGUAGE OverloadedStrings #-}
-- | Whether formatting changed what a module says.
module Tilia.EquivalenceSpec (spec) where
import Data.Maybe (isJust)
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Hs (HsModule)
import GHC.Hs.Extension (GhcPs)
import Test.Hspec
import Tilia.Equivalence
import Tilia.Parser (pmModule)
import Tilia.TestUtils (parsed)
spec :: Spec
spec = do
describe "what a formatter is allowed to do" $ do
it "sees past layout" $
"module M where\nf x = x\n" `saysTheSameAs` "module M where\n\nf x = x\n"
it "sees past a line broken in a different place" $
"module M where\nf x = x + 1\n"
`saysTheSameAs` "module M where\nf x =\n x\n + 1\n"
it "sees past the two spellings of a qualified import" $
"module M where\nimport qualified Data.Map as M\n"
`saysTheSameAs` "module M where\nimport Data.Map qualified as M\n"
it "sees past the order the imports were written in" $
"module M where\nimport Data.Set\nimport Data.Map\n"
`saysTheSameAs` "module M where\nimport Data.Map\nimport Data.Set\n"
it "sees past the brackets around a deriving clause" $
"module M where\ndata T = T deriving Eq\n"
`saysTheSameAs` "module M where\ndata T = T deriving (Eq)\n"
it "sees past an empty context" $
"module M where\nclass () => C a where\n m :: a\n"
`saysTheSameAs` "module M where\nclass C a where\n m :: a\n"
it "sees past the brackets around one constraint" $
"module M where\nf :: (Show a) => a -> a\nf x = x\n"
`saysTheSameAs` "module M where\nf :: Show a => a -> a\nf x = x\n"
it "sees past a documentation comment set differently" $
"module M where\n\n-- | Says something.\nf :: Int\nf = 1\n"
`saysTheSameAs` "module M where\n\n-- | Says something.\nf :: Int\nf = 1\n"
it "sees past a documentation comment that says nothing at all" $
"module M where\n\n-- |\nf :: Int\nf = 1\n"
`saysTheSameAs` "module M where\n\nf :: Int\nf = 1\n"
it "sees past comments, which are not the tree's business" $
"module M where\n\n-- a remark\nf :: Int\nf = 1\n"
`saysTheSameAs` "module M where\n\nf :: Int\nf = 1\n"
describe "what it must not let through" $ do
it "catches a changed literal" $
"module M where\nf = 1\n" `saysSomethingElseThan` "module M where\nf = 2\n"
it "catches a changed name" $
"module M where\nf x = x\n" `saysSomethingElseThan` "module M where\nf x = y\n"
it "catches an operator regrouped" $
"module M where\nf = a + b * c\n"
`saysSomethingElseThan` "module M where\nf = (a + b) * c\n"
it "catches a declaration dropped" $
"module M where\nf = 1\ng = 2\n" `saysSomethingElseThan` "module M where\nf = 1\n"
it "catches an import dropped" $
"module M where\nimport Data.Map\nimport Data.Set\n"
`saysSomethingElseThan` "module M where\nimport Data.Map\n"
it "catches an import list losing an entry" $
"module M where\nimport Data.List (sort, nub)\n"
`saysSomethingElseThan` "module M where\nimport Data.List (sort)\n"
it "catches an import that stopped being qualified" $
"module M where\nimport qualified Data.Map as M\n"
`saysSomethingElseThan` "module M where\nimport Data.Map as M\n"
it "catches an export list losing an entry" $
"module M (f, g) where\nf = 1\ng = 2\n"
`saysSomethingElseThan` "module M (f) where\nf = 1\ng = 2\n"
it "catches a documentation comment losing a word" $
"module M where\n\n-- | Says something.\nf :: Int\nf = 1\n"
`saysSomethingElseThan` "module M where\n\n-- | Says.\nf :: Int\nf = 1\n"
it "says where the difference is, not merely that there is one" $
case syntaxDifference (treeOf "module M where\nf = 1\n") (treeOf "module M where\nf = 2\n") of
Nothing -> expectationFailure "found no difference"
Just why -> do
why `shouldSatisfy` T.isInfixOf "HsOverLit"
why `shouldSatisfy` T.isInfixOf "1 became 2"
describe "the comments a formatter must carry over" $ do
it "is content when they all came through" $
"module M where\n\n-- a remark\nf = 1\n"
`keepsTheCommentsOf` "module M where\n\n-- a remark\nf = 1\n"
it "notices one that went missing" $
"module M where\n\n-- a remark\nf = 1\n"
`losesTheCommentsOf` "module M where\n\nf = 1\n"
it "notices one that was invented" $
"module M where\n\nf = 1\n"
`losesTheCommentsOf` "module M where\n\n-- a remark\nf = 1\n"
it "notices a pragma that went missing, and names it" $
case difference "{-# LANGUAGE LambdaCase #-}\nmodule M where\nf = 1\n" "module M where\nf = 1\n" of
Nothing -> expectationFailure "let the pragma go"
Just why -> do
why `shouldSatisfy` T.isInfixOf "lost the pragma"
why `shouldSatisfy` T.isInfixOf "LambdaCase"
it "notices a pragma that was invented" $
case difference "module M where\nf = 1\n" "{-# LANGUAGE LambdaCase #-}\nmodule M where\nf = 1\n" of
Nothing -> expectationFailure "let the pragma through"
Just why -> why `shouldSatisfy` T.isInfixOf "invented the pragma"
it "does not mind the order of what stands above the module header" $
"{-# LANGUAGE LambdaCase #-}\n{-# LANGUAGE TupleSections #-}\nmodule M where\nf = 1\n"
`keepsTheCommentsOf` "{-# LANGUAGE TupleSections #-}\n{-# LANGUAGE LambdaCase #-}\nmodule M where\nf = 1\n"
it "does not mind a comment that moved with the import it belongs to" $
"module M where\n\n-- about Set\nimport Data.Set\n\n-- about Map\nimport Data.Map\n"
`keepsTheCommentsOf` "module M where\n\n-- about Map\nimport Data.Map\n\n-- about Set\nimport Data.Set\n"
it "does not mind a comment carried on under the last import moving with it" $
"module M where\nimport Data.Maybe\nimport Data.Char () -- not needed, just\n -- re-exported\n"
`keepsTheCommentsOf` "module M where\n\nimport Data.Char () -- not needed, just\n -- re-exported\nimport Data.Maybe\n"
it "notices a comment carried on under the last import going missing" $
"module M where\nimport Data.Maybe\nimport Data.Char () -- not needed, just\n -- re-exported\n"
`losesTheCommentsOf` "module M where\n\nimport Data.Char () -- not needed, just\nimport Data.Maybe\n"
describe "what the header puts in force" $ do
it "does not mind language pragmas sorted where that changes nothing" $
"{-# LANGUAGE NoStarIsType #-}\n{-# LANGUAGE TypeOperators #-}\nmodule M where\n"
`keepsTheCommentsOf` "{-# LANGUAGE TypeOperators #-}\n{-# LANGUAGE NoStarIsType #-}\nmodule M where\n"
it "does not mind options sorted below the language pragmas" $
"{-# OPTIONS_GHC -Wall #-}\n{-# LANGUAGE LambdaCase #-}\nmodule M where\n"
`keepsTheCommentsOf` "{-# LANGUAGE LambdaCase #-}\n{-# OPTIONS_GHC -Wall #-}\nmodule M where\n"
it "does not mind Haddock's options sorted below GHC's" $
"{-# OPTIONS_HADDOCK not-home #-}\n{-# OPTIONS_GHC -Wall #-}\nmodule M where\n"
`keepsTheCommentsOf` "{-# OPTIONS_GHC -Wall #-}\n{-# OPTIONS_HADDOCK not-home #-}\nmodule M where\n"
it "notices Haddock's options coming out in another order" $
differsBecause
"{-# OPTIONS_HADDOCK hide #-}\n{-# OPTIONS_HADDOCK not-home #-}\nmodule M where\n"
"{-# OPTIONS_HADDOCK not-home #-}\n{-# OPTIONS_HADDOCK hide #-}\nmodule M where\n"
"changed the OPTIONS_HADDOCK pragmas"
it "notices the flags of options coming out in another order" $
differsBecause
"{-# OPTIONS_GHC -Wall #-}\n{-# OPTIONS -Wno-orphans #-}\nmodule M where\n"
"{-# OPTIONS -Wno-orphans #-}\n{-# OPTIONS_GHC -Wall #-}\nmodule M where\n"
"changed the flags of the options pragmas"
it "notices an options pragma written twice coming out once" $
differsBecause
"{-# OPTIONS_GHC -fplugin-opt=P:x #-}\n{-# OPTIONS_GHC -fplugin-opt=P:x #-}\nmodule M where\n"
"{-# OPTIONS_GHC -fplugin-opt=P:x #-}\nmodule M where\n"
"changed the flags of the options pragmas"
it "notices an extension turned off by names moved past each other" $
differsBecause
"{-# LANGUAGE NoLambdaCase #-}\n{-# LANGUAGE LambdaCase #-}\nmodule M where\n"
"{-# LANGUAGE LambdaCase #-}\n{-# LANGUAGE NoLambdaCase #-}\nmodule M where\n"
"switches LambdaCase"
it "notices an extension turned off past one implying it" $
differsBecause
"{-# LANGUAGE NoMonoLocalBinds #-}\n{-# LANGUAGE TypeFamilies #-}\nmodule M where\n"
"{-# LANGUAGE TypeFamilies #-}\n{-# LANGUAGE NoMonoLocalBinds #-}\nmodule M where\n"
"switches MonoLocalBinds"
it "notices the Prelude turned back on past RebindableSyntax" $
differsBecause
"{-# LANGUAGE ImplicitPrelude #-}\n{-# LANGUAGE RebindableSyntax #-}\nmodule M where\n"
"{-# LANGUAGE RebindableSyntax #-}\n{-# LANGUAGE ImplicitPrelude #-}\nmodule M where\n"
"switches ImplicitPrelude"
it "notices an option turning off what a language pragma turns on" $
differsBecause
"{-# OPTIONS_GHC -XNoLambdaCase #-}\n{-# LANGUAGE LambdaCase #-}\nmodule M where\n"
"{-# LANGUAGE LambdaCase #-}\n{-# OPTIONS_GHC -XNoLambdaCase #-}\nmodule M where\n"
"switches LambdaCase"
it "notices a pragma moved into the header, though GHC's defaults have it" $
differsBecause
"module M where\n{-# LANGUAGE MultiParamTypeClasses #-}\n"
"{-# LANGUAGE MultiParamTypeClasses #-}\nmodule M where\n"
"MultiParamTypeClasses"
it "notices another edition named last" $
differsBecause
"{-# LANGUAGE Haskell2010 #-}\n{-# LANGUAGE GHC2021 #-}\nmodule M where\n"
"{-# LANGUAGE GHC2021 #-}\n{-# LANGUAGE Haskell2010 #-}\nmodule M where\n"
"changed the edition in force"
it "does not mind a pragma below the header, which GHC does not read" $
"module M where\n{-# LANGUAGE NoLambdaCase #-}\n{-# LANGUAGE LambdaCase #-}\nf = 1\n"
`keepsTheCommentsOf` "module M where\n{-# LANGUAGE LambdaCase #-}\n{-# LANGUAGE NoLambdaCase #-}\nf = 1\n"
----------------------------------------------------------------------------
-- Helpers
-- | Two modules that a formatter could turn one into the other.
saysTheSameAs :: Text -> Text -> Expectation
saysTheSameAs went came =
syntaxDifference (treeOf went) (treeOf came) `shouldBe` Nothing
-- | Two modules that no formatter may turn one into the other.
saysSomethingElseThan :: Text -> Text -> Expectation
saysSomethingElseThan went came =
syntaxDifference (treeOf went) (treeOf came) `shouldSatisfy` isJust
keepsTheCommentsOf :: Text -> Text -> Expectation
keepsTheCommentsOf went came = difference went came `shouldBe` Nothing
-- | Two modules that differ, for a reason that says the given thing.
differsBecause :: Text -> Text -> Text -> Expectation
differsBecause went came reason = case difference went came of
Nothing -> expectationFailure "saw no difference"
Just why -> why `shouldSatisfy` T.isInfixOf reason
losesTheCommentsOf :: Text -> Text -> Expectation
losesTheCommentsOf went came =
difference went came `shouldSatisfy` isJust
-- | What 'commentDifference' makes of two spellings of a module.
difference :: Text -> Text -> Maybe Text
difference went came = commentDifference (parsed went) (parsed came)
treeOf :: Text -> HsModule GhcPs
treeOf = pmModule . parsed