{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} -- | Comment handling. module Floskell.Comments ( filterCommentLike, annotateWithComments ) where import Control.Arrow ( first, second ) import Control.Monad.State.Strict import Data.Foldable ( traverse_ ) import Data.List ( foldl', isPrefixOf ) import qualified Data.Map.Strict as M import Data.Text.Lazy ( Text ) import qualified Data.Text.Lazy as TL import Floskell.Types import Language.Haskell.Exts.SrcLoc ( SrcSpanInfo(..) ) data FilterMode = Normal | CppContinuation | Unformatted deriving ( Eq, Ord, Enum, Show ) data FilterState = FilterState { stMode :: !FilterMode , stLines :: [Text] , stComments :: [Comment] } deriving ( Show ) -- Order by start of span, larger spans before smaller spans. newtype OrderByStart = OrderByStart SrcSpan deriving ( Eq ) instance Ord OrderByStart where compare (OrderByStart l) (OrderByStart r) = compare (srcSpanStartLine l) (srcSpanStartLine r) `mappend` compare (srcSpanStartColumn l) (srcSpanStartColumn r) `mappend` compare (srcSpanEndLine r) (srcSpanEndLine l) `mappend` compare (srcSpanEndColumn r) (srcSpanEndColumn l) -- Order by end of span, smaller spans before larger spans. newtype OrderByEnd = OrderByEnd SrcSpan deriving ( Eq ) instance Ord OrderByEnd where compare (OrderByEnd l) (OrderByEnd r) = compare (srcSpanEndLine l) (srcSpanEndLine r) `mappend` compare (srcSpanEndColumn l) (srcSpanEndColumn r) `mappend` compare (srcSpanStartLine r) (srcSpanStartLine l) `mappend` compare (srcSpanStartColumn r) (srcSpanStartColumn l) -- | Remove comment-like blocks from input source, replacing them with -- blank likes to keep SrcSpan information intact. filterCommentLike :: [Text] -> ([Text], [Comment]) filterCommentLike = finish . foldl' go start . zip [ 1 .. ] where start = FilterState Normal [] [] go s@FilterState{..} (n, l) = case stMode of Normal -> if | isShebangLine l -> addComment s Normal IgnoredLine n l | isCppLine l -> let newMode = if isCppContinuation l then CppContinuation else Normal in addComment s newMode PreprocessorDirective n l | isBeginIgnore l -> addComment s Unformatted IgnoredLine n l | otherwise -> addLine s l CppContinuation -> let newMode = if isCppContinuation l then CppContinuation else Normal in addComment s newMode PreprocessorDirective n l Unformatted -> let newMode = if isEndIgnore l then Normal else Unformatted in addComment s newMode IgnoredLine n l finish s = (reverse $ stLines s, reverse $ stComments s) addLine s@FilterState{..} l = s { stLines = l : stLines } addComment s@FilterState{..} mode t n l = s { stMode = mode , stLines = "" : stLines , stComments = makeComment t n l : stComments } makeComment t n l = Comment t (SrcSpan "" n 1 n (fromIntegral $ TL.length l + 1)) (TL.unpack l) isShebangLine = TL.isPrefixOf "#!" isCppLine src = any (`TL.isPrefixOf` src) [ "#define" , "#elif" , "#else" , "#end" , "#enum" , "#error" , "#if" , "#include" , "#undef" , "#warning" ] isCppContinuation = TL.isSuffixOf "\\" isBeginIgnore src = any (`TL.isPrefixOf` src) [ "-- floskell-begin-disable-region" , "-- floskell-disable" , "{- floskell-disable" ] isEndIgnore src = any (`TL.isPrefixOf` src) [ "-- floskell-end-disable-region" , "-- floskell-enable" , "{- floskell-enable" ] onSameLine :: SrcSpan -> SrcSpan -> Bool onSameLine ss ss' = srcSpanEndLine ss == srcSpanStartLine ss' isAfterComment :: Comment -> Bool isAfterComment (Comment PreprocessorDirective _ str) = "#endif" `isPrefixOf` str isAfterComment (Comment _ _ str) = take 1 (dropWhile (== ' ') $ dropWhile (== '-') str) == "^" isAlignedWith :: Comment -> Comment -> Bool isAlignedWith (Comment _ before _) (Comment _ after _) = srcSpanEndLine before == srcSpanStartLine after - 1 && srcSpanStartColumn before == srcSpanStartColumn after -- | Annotate the AST with comments. annotateWithComments :: Traversable ast => ast SrcSpanInfo -> [Comment] -> ast NodeInfo annotateWithComments src comments = evalState (do traverse_ assignComment comments traverse transferComments src) nodeinfos where nodeinfos :: M.Map SrcSpanInfo ([Comment], [Comment]) nodeinfos = foldr (\ssi -> M.insert ssi ([], [])) M.empty src -- Assign a single comment to the right AST node assignComment :: Comment -> State (M.Map SrcSpanInfo ([Comment], [Comment])) () assignComment comment@(Comment _ cspan _) = case surrounding comment of (Nothing, Nothing) -> error "No target nodes for comment" (Just before, Nothing) -> insertComment After before (Nothing, Just after) -> insertComment Before after (Just before, Just after) -> if srcInfoSpan before `onSameLine` cspan || isAfterComment comment then insertComment After before else do cmts <- gets (M.! before) case cmts of -- We've already collected comments for this -- node and this comment is a continuation. (_, c' : _) | c' `isAlignedWith` comment -> insertComment After before -- The comment does not belong to this node. -- If there is a node following this comment, -- assign it to that node, else keep it here, -- anyway. _ -> insertComment Before after where insertComment :: Location -> SrcSpanInfo -> State (M.Map SrcSpanInfo ([Comment], [Comment])) () insertComment Before ssi = modify $ M.adjust (first (comment :)) ssi insertComment After ssi = modify $ M.adjust (second (comment :)) ssi -- Transfer collected comments into the AST. transferComments :: SrcSpanInfo -> State (M.Map SrcSpanInfo ([Comment], [Comment])) NodeInfo transferComments ssi = do (c, c') <- gets (M.! ssi) -- Sometimes, there are multiple AST nodes with the same -- SrcSpan. Make sure we assign comments to only one of -- them. modify $ M.insert ssi ([], []) return $ NodeInfo (srcInfoSpan ssi) (reverse c) (reverse c') surrounding (Comment _ ss _) = (nodeBefore ss, nodeAfter ss) nodeBefore ss = fmap snd $ OrderByEnd ss `M.lookupLT` spansByEnd nodeAfter ss = fmap snd $ OrderByStart ss `M.lookupGT` spansByStart spansByStart = foldr (\ssi -> M.insert (OrderByStart $ srcInfoSpan ssi) ssi) M.empty src spansByEnd = foldr (\ssi -> M.insert (OrderByEnd $ srcInfoSpan ssi) ssi) M.empty src