packages feed

tilia-0.0.1.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.Comments (Comment)
import Tilia.Equivalence
import Tilia.Parser
  ( ParsedModule,
    defaultParserConfig,
    describeParseError,
    parseModule,
    pmModule,
    pmSource,
  )
import Tilia.Source (comments)

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"

----------------------------------------------------------------------------
-- 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

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
    (treeOf went, treeOf came)
    (commentsOf went)
    (commentsOf came)

treeOf :: Text -> HsModule GhcPs
treeOf = pmModule . parsed

commentsOf :: Text -> [Comment]
commentsOf = comments . pmSource . parsed

parsed :: Text -> ParsedModule
parsed source = case parseModule defaultParserConfig "Test.hs" source of
  Left problem -> error (T.unpack (describeParseError problem))
  Right m -> m