tilia-0.0.1.0: tests/Tilia/CommentsSpec.hs
{-# LANGUAGE OverloadedStrings #-}
-- | Extraction of the comment stream from real source text, and the
-- normalizations applied on the way.
module Tilia.CommentsSpec (spec) where
import Data.List.NonEmpty qualified as NE
import Data.Text (Text)
import Data.Text qualified as T
import Test.Hspec
import Tilia.Comments
import Tilia.Comments.Attach
import Tilia.Doc
import Tilia.Doc.Combinators
import Tilia.Parser
import Tilia.Source (comments)
import Tilia.Span
spec :: Spec
spec = do
describe "extraction" $ do
it "finds a comment on its own line" $
bodies "module M where\n-- a comment\nx = 1\n"
`shouldBe` [["-- a comment"]]
it "finds several, in source order" $
bodies "module M where\n-- one\nx = 1\n-- two\ny = 2\n"
`shouldBe` [["-- one"], ["-- two"]]
it "finds a block comment and keeps its lines" $
bodies "module M where\n{- one\n two -}\nx = 1\n"
`shouldBe` [["{- one", " two -}"]]
it "reports no comments when there are none" $
bodies "module M where\nx = 1\n" `shouldBe` []
describe "trailing" $ do
it "marks a comment that follows code on its line" $
trailings "module M where\nx = 1 -- here\n" `shouldBe` [True]
it "does not mark one on a line of its own" $
trailings "module M where\n-- here\nx = 1\n" `shouldBe` [False]
it "does not mark one indented on a line of its own" $
trailings "module M where\nx =\n -- here\n 1\n" `shouldBe` [False]
-- The lexer counts a tab as advancing to the next multiple of eight, so a
-- line holding one has more columns than characters. Everything here works
-- by cutting the source at a column the compiler reported, and cutting at
-- the wrong place is not a crash but a comment that quietly believes it
-- has nothing after it.
describe "lines indented with tabs" $ do
it "sees the code before a comment" $
trailings "module M where\n\tx = 1 -- here\n" `shouldBe` [True]
it "sees that a comment has the line to itself" $
trailings "module M where\n\t-- here\n\tx = 1\n" `shouldBe` [False]
it "sees the code after a block comment" $
followeds "module M where\n\tx = f {- here -} 1\n" `shouldBe` [True]
it "sees that nothing follows a block comment" $
followeds "module M where\n\tx = f 1 {- here -}\n" `shouldBe` [False]
it "takes the comment's text and no more" $
bodies "module M where\n\tx = f {- here -} 1\n" `shouldBe` [["{- here -}"]]
it "dedents a block comment by what precedes it" $
bodies "module M where\n\t{- one\n\t two -}\nx = 1\n"
`shouldBe` [["{- one", " two -}"]]
describe "normalization: space after dashes" $ do
it "adds a missing space" $
bodies "module M where\n--tight\nx = 1\n" `shouldBe` [["-- tight"]]
it "leaves an existing space alone" $
bodies "module M where\n-- loose\nx = 1\n" `shouldBe` [["-- loose"]]
it "leaves a divider alone" $
bodies "module M where\n-------\nx = 1\n" `shouldBe` [["-------"]]
it "does not touch dashes inside a block comment" $
bodies "module M where\n{--tight-}\nx = 1\n" `shouldBe` [["{--tight-}"]]
describe "normalization: trailing whitespace"
$ it "strips it from every line"
$ bodies "module M where\n{- one \n two \n three -}\nx = 1\n"
`shouldBe` [["{- one", " two", " three -}"]]
describe "normalization: dedent" $ do
it "drops the comment\'s own start column, not all indentation" $
bodies "module M where\nx =\n {- one\n two\n three -}\n 1\n"
`shouldBe` [["{- one", " two", " three -}"]]
it "keeps relative indentation between continuation lines" $
bodies "module M where\nx =\n {- one\n two\n three -}\n 1\n"
`shouldBe` [["{- one", " two", " three -}"]]
it "leaves an unindented comment alone" $
bodies "module M where\n{- one\n two -}\nx = 1\n"
`shouldBe` [["{- one", " two -}"]]
-- Widening is not part of extraction: whether a doc comment's trigger is
-- tidied or escaped depends on whether the syntax tree turned out to
-- carry it, which nothing here knows. So it is asked for.
describe "normalization: doc trigger" $ do
it "widens a tight trigger" $
widened "module M where\n-- |Foo\nx = 1\n" `shouldBe` [["-- | Foo"]]
it "leaves an already spaced trigger alone" $
widened "module M where\n-- | Foo\nx = 1\n" `shouldBe` [["-- | Foo"]]
it "widens a caret trigger" $
widened "module M where\nx = 1\n-- ^Foo\n" `shouldBe` [["-- ^ Foo"]]
it "widens a section trigger, keeping its stars" $
widened "module M where\n-- **Foo\nx = 1\n" `shouldBe` [["-- ** Foo"]]
it "leaves a named anchor alone" $
widened "module M where\n-- $section\nx = 1\n" `shouldBe` [["-- $section"]]
it "leaves a trigger with nothing after it alone" $
widened "module M where\n-- |\nx = 1\n" `shouldBe` [["-- |"]]
it "shifts continuation lines to match" $
widened "module M where\n{-|Foo\n bar\n-}\nx = 1\n"
`shouldBe` [["{-| Foo", " bar", " -}"]]
it "does not widen an ordinary line comment" $
widened "module M where\n-- x|y\nz = 1\n" `shouldBe` [["-- x|y"]]
describe "pragmas" $ do
it "recognises one" $
(commentPragma <$> commentsIn "{-# LANGUAGE CPP #-}\nmodule M where\nx = 1\n")
`shouldBe` [Just (Pragma "LANGUAGE" "CPP")]
it "upper-cases the name" $
(fmap pragmaName . commentPragma <$> commentsIn "{-# language CPP #-}\nmodule M where\nx = 1\n")
`shouldBe` [Just "LANGUAGE"]
it "is not fooled by an ordinary block comment" $
(commentPragma <$> commentsIn "module M where\n{- not a pragma -}\nx = 1\n")
`shouldBe` [Nothing]
it "reports the header boundary" $
headerLine "{-# LANGUAGE CPP #-}\nmodule M where\nimport Data.List\nx = 1\n"
`shouldBe` Just 3
it "reports no boundary for a module with only a header" $
headerLine "{-# LANGUAGE CPP #-}\nmodule M where\n" `shouldBe` Nothing
describe "what is deliberately not normalized" $ do
it "keeps blank lines inside a comment" $
bodies "module M where\n{- one\n\n\n two -}\nx = 1\n"
`shouldBe` [["{- one", "", "", " two -}"]]
it "does not escape a Haddock trigger" $
bodies "module M where\nx = 1\n\n-- | not attached to anything\n"
`shouldBe` [["-- | not attached to anything"]]
describe "renderComment"
$ it "joins the lines back with newlines"
$ renderComment <$> commentsIn "module M where\n{- one\n two -}\nx = 1\n"
`shouldBe` ["{- one\n two -}"]
describe "attachment" $ do
it "puts a comment before the node it precedes" $
let c = one "module M where\n-- note\nx = 1\n"
d = located (mkSpan (3, 1) (3, 5)) (txt "x = 1")
in render (attachComments [c] d) `shouldBe` "-- note\nx = 1\n"
it "keeps a trailing comment on the same line" $
let c = one "module M where\nx = 1 -- note\n"
d = located (mkSpan (2, 1) (2, 6)) (txt "x = 1")
in render (attachComments [c] d) `shouldBe` "x = 1 -- note\n"
it "descends into the node that contains the comment" $
let c = one "module M where\nx =\n -- note\n 1\n"
inner = located (mkSpan (4, 3) (4, 4)) (txt "1")
d = located (mkSpan (2, 1) (4, 4)) (txt "x =" <> indent (hardBreak <> inner))
in render (attachComments [c] d) `shouldBe` "x =\n -- note\n 1\n"
it "appends a comment that follows every node rather than dropping it" $
let c = one "module M where\nx = 1\n-- after\n"
d = located (mkSpan (2, 1) (2, 6)) (txt "x = 1")
in
-- The blank line is added: a comment after everything is about the
-- file rather than about the line it happens to follow.
render (attachComments [c] d) `shouldBe` "x = 1\n\n-- after\n"
it "reaches inside a variant, whichever branch renders" $
let c = one "module M where\nx = 1 -- note\n"
n = located (mkSpan (2, 1) (2, 6)) (txt "x = 1")
d = variant n (txt "(" <> n <> txt ")")
in ( countOf "-- note" (render (flat (attachComments [c] d))),
countOf "-- note" (render (broken (attachComments [c] d)))
)
`shouldBe` (1, 1)
it "places a comment inside an empty construct" $
let c = one "module M where\nx = [ -- note\n ]\n"
d =
located (mkSpan (2, 5) (3, 4)) $
txt "[" <> emptyAnchor (mkSpan (3, 4) (3, 4)) <> txt "]"
in render (attachComments [c] d) `shouldBe` "[ -- note\n]\n"
it "emits every comment exactly once" $
let cs = commentsIn "module M where\n-- a\nx = 1 -- b\n-- c\ny = 2\n"
d = located (mkSpan (3, 1) (5, 6)) (txt "code")
out = render (attachComments cs d)
in (length cs, countOf "-- a" out, countOf "-- b" out, countOf "-- c" out)
`shouldBe` (3, 1, 1, 1)
----------------------------------------------------------------------------
-- Helpers
render :: Doc -> Text
render = printDoc defaultRenderOptions
one :: Text -> Comment
one src = case commentsIn src of
(c : _) -> c
[] -> error "the test input had no comments"
countOf :: Text -> Text -> Int
countOf needle = length . T.breakOnAll needle
commentsIn :: Text -> [Comment]
commentsIn src =
case parseModule defaultParserConfig "test.hs" src of
Left _ -> error "the test input did not parse"
Right pm -> comments (pmSource pm)
bodies :: Text -> [[Text]]
bodies = map (NE.toList . commentBody) . commentsIn
-- | The bodies a doc comment comes out with once its trigger is tidied.
widened :: Text -> [[Text]]
widened = map (NE.toList . commentBody . widenTrigger) . commentsIn
trailings :: Text -> [Bool]
trailings = map commentTrailing . commentsIn
followeds :: Text -> [Bool]
followeds = map commentFollowed . commentsIn
headerLine :: Text -> Maybe Int
headerLine src = case parseModule defaultParserConfig "test.hs" src of
Left _ -> error "the test input did not parse"
Right pm -> spanStartLine <$> pmHeaderEnd pm