fourmolu-0.4.0.0: src/Ormolu/Parser/CommentStream.hs
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
-- | Functions for working with comment stream.
module Ormolu.Parser.CommentStream
( -- * Comment stream
CommentStream (..),
mkCommentStream,
showCommentStream,
-- * Comment
Comment (..),
unComment,
hasAtomsBefore,
isMultilineComment,
)
where
import Data.Char (isSpace)
import Data.Data (Data)
import qualified Data.List as L
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.List.NonEmpty as NE
import Data.Maybe (mapMaybe)
import qualified GHC.Parser.Annotation as GHC
import qualified GHC.Parser.Lexer as GHC
import GHC.Types.SrcLoc
import Ormolu.Parser.Pragma
import Ormolu.Utils (onTheSameLine, showOutputable)
----------------------------------------------------------------------------
-- Comment stream
-- | A stream of 'RealLocated' 'Comment's in ascending order with respect to
-- beginning of corresponding spans.
newtype CommentStream = CommentStream [RealLocated Comment]
deriving (Eq, Data, Semigroup, Monoid)
-- | Create 'CommentStream' from 'GHC.PState'. The pragmas are
-- removed from the 'CommentStream'.
mkCommentStream ::
-- | Original input
String ->
-- | Parser state to use for comment extraction
GHC.PState ->
-- | Stack header, pragmas, and comment stream
( Maybe (RealLocated Comment),
[([RealLocated Comment], Pragma)],
CommentStream
)
mkCommentStream input pstate =
( mstackHeader,
pragmas,
CommentStream comments
)
where
(comments, pragmas) = extractPragmas input rawComments1
(rawComments1, mstackHeader) = extractStackHeader rawComments0
rawComments0 =
L.sortOn (realSrcSpanStart . getRealSrcSpan) $
mapMaybe (liftMaybe . fmap unAnnotationComment) (GHC.comment_q pstate)
++ concatMap
(mapMaybe (liftMaybe . fmap unAnnotationComment) . snd)
(GHC.annotations_comments pstate)
-- | Pretty-print a 'CommentStream'.
showCommentStream :: CommentStream -> String
showCommentStream (CommentStream xs) =
unlines $
showComment <$> xs
where
showComment (L l str) = showOutputable l ++ " " ++ show str
----------------------------------------------------------------------------
-- Comment
-- | A wrapper for a single comment. The 'Bool' indicates whether there were
-- atoms before beginning of the comment in the original input. The
-- 'NonEmpty' list inside contains lines of multiline comment @{\- … -\}@ or
-- just single item\/line otherwise.
data Comment = Comment Bool (NonEmpty String)
deriving (Eq, Show, Data)
-- | Normalize comment string. Sometimes one multi-line comment is turned
-- into several lines for subsequent outputting with correct indentation for
-- each line.
mkComment ::
-- | Lines of original input with their indices
[(Int, String)] ->
-- | Raw comment string
RealLocated String ->
-- | Remaining lines of original input and the constructed 'Comment'
([(Int, String)], RealLocated Comment)
mkComment ls (L l s) = (ls', comment)
where
comment =
L l . Comment atomsBefore . removeConseqBlanks . fmap dropTrailing $
if "{-" `L.isPrefixOf` s
then case NE.nonEmpty (lines s) of
Nothing -> s :| []
Just (x :| xs) ->
let getIndent y =
if all isSpace y
then startIndent
else length (takeWhile isSpace y)
n = minimum (startIndent : fmap getIndent xs)
in x :| (drop n <$> xs)
else s :| []
(atomsBefore, ls') =
case dropWhile ((< commentLine) . fst) ls of
[] -> (False, [])
((_, i) : ls'') ->
case take 2 (dropWhile isSpace i) of
"--" -> (False, ls'')
"{-" -> (False, ls'')
_ -> (True, ls'')
dropTrailing = L.dropWhileEnd isSpace
startIndent = srcSpanStartCol l - 1
commentLine = srcSpanStartLine l
-- | Get a collection of lines from a 'Comment'.
unComment :: Comment -> NonEmpty String
unComment (Comment _ xs) = xs
-- | Check whether the 'Comment' had some non-whitespace atoms in front of
-- it in the original input.
hasAtomsBefore :: Comment -> Bool
hasAtomsBefore (Comment atomsBefore _) = atomsBefore
-- | Is this comment multiline-style?
isMultilineComment :: Comment -> Bool
isMultilineComment (Comment _ (x :| _)) = "{-" `L.isPrefixOf` x
----------------------------------------------------------------------------
-- Helpers
-- | Detect and extract stack header if it is present.
extractStackHeader ::
-- | Comment stream to analyze
[RealLocated String] ->
([RealLocated String], Maybe (RealLocated Comment))
extractStackHeader = \case
[] -> ([], Nothing)
(x : xs) ->
let comment = snd (mkComment [] x)
in if isStackHeader (unRealSrcSpan comment)
then (xs, Just comment)
else (x : xs, Nothing)
where
isStackHeader (Comment _ (x :| _)) =
"stack" `L.isPrefixOf` dropWhile isSpace (drop 2 x)
-- | Extract pragmas and their associated comments.
extractPragmas ::
-- | Input
String ->
-- | Comment stream to analyze
[RealLocated String] ->
([RealLocated Comment], [([RealLocated Comment], Pragma)])
extractPragmas input = go initialLs id id
where
initialLs = zip [1 ..] (lines input)
go ls csSoFar pragmasSoFar = \case
[] -> (csSoFar [], pragmasSoFar [])
(x : xs) ->
case parsePragma (unRealSrcSpan x) of
Nothing ->
let (ls', x') = mkComment ls x
in go ls' (csSoFar . (x' :)) pragmasSoFar xs
Just pragma ->
let combined ys = (csSoFar ys, pragma)
go' ls' ys rest = go ls' id (pragmasSoFar . (combined ys :)) rest
in case xs of
[] -> go' ls [] xs
(y : ys) ->
let (ls', y') = mkComment ls y
in if onTheSameLine
(RealSrcSpan (getRealSrcSpan x) Nothing)
(RealSrcSpan (getRealSrcSpan y) Nothing)
then go' ls' [y'] ys
else go' ls [] xs
-- | Get a 'String' from 'GHC.AnnotationComment'.
unAnnotationComment :: GHC.AnnotationComment -> Maybe String
unAnnotationComment = \case
GHC.AnnDocCommentNext _ -> Nothing -- @-- |@
GHC.AnnDocCommentPrev _ -> Nothing -- @-- ^@
GHC.AnnDocCommentNamed _ -> Nothing -- @-- $@
GHC.AnnDocSection _ _ -> Nothing -- @-- *@
GHC.AnnDocOptions s -> Just s
GHC.AnnLineComment s -> Just s
GHC.AnnBlockComment s -> Just s
liftMaybe :: GenLocated l (Maybe a) -> Maybe (GenLocated l a)
liftMaybe = \case
L _ Nothing -> Nothing
L l (Just a) -> Just (L l a)
-- | Remove consecutive blank lines.
removeConseqBlanks :: NonEmpty String -> NonEmpty String
removeConseqBlanks (x :| xs) = x :| go (null x) id xs
where
go seenBlank acc = \case
[] -> acc []
(y : ys) ->
if seenBlank && null y
then go True acc ys
else go (null y) (acc . (y :)) ys