pinned-warnings-0.1.0.4: src/Internal/FixWarnings.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Internal.FixWarnings
( fixWarning
, fixRedundancyWarning
) where
import Control.Applicative ((<|>))
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.State
import Data.Bifunctor (first)
import qualified Data.ByteString.Char8 as BS
import Data.Char (isSpace)
import Data.Monoid (Alt(..))
import qualified Data.Map.Strict as M
import qualified System.Directory as Dir
import qualified Text.ParserCombinators.ReadP as P
import qualified Internal.GhcFacade as Ghc
import Internal.Types
-- | Fixes applicable warning and returns 'False' if all warnings for the
-- corresponding span should be removed.
fixWarning :: WarningsWithModDate -> IO WarningsWithModDate
fixWarning (Alt (Just modifiedAt), MonoidMap warnMap) = do
-- State is used to keep the contents of a source file in memory while all
-- applicable warnings for that file are fixed.
(pairs, files) <- (`runStateT` M.empty)
. flip filterM (reverse $ M.toList warnMap) $ \case
((start, _), warnSet)
| Alt (Just reWarn) -- Take the first redundancy warning parsed
<- foldMap (Alt . parseRedundancyWarn) warnSet
-> do
let file = Ghc.unpackFS $ Ghc.srcLocFile start
mCached <- gets (M.lookup file)
srcLines <-
maybe (liftIO . fmap BS.lines $ BS.readFile file)
pure
mCached
fileModified <- liftIO $ Dir.getModificationTime file
if fileModified /= modifiedAt
then do
-- Do not attempt to edit if file has been touched since last reload
liftIO . putStrLn
$ "'" <> file
<> "' has been modified since last compiled. Reload and try again."
pure True
-- attempt to fix the warning
else do
let startLine = Ghc.srcLocLine start
mNewSrcLines =
fixRedundancyWarning startLine reWarn srcLines
case mNewSrcLines of
Nothing -> pure True
Just newSrcLines -> do
modify' $ M.insert file newSrcLines
pure False
_ -> pure True
-- write the changes to the file
_ <- M.traverseWithKey
(\file ls -> do
BS.writeFile file $ BS.unlines ls
putStrLn $ "'" <> file <> "' has been edited"
)
files
pure (Alt Nothing, MonoidMap $ M.fromList pairs)
fixWarning w = pure w
-- TODO handle operators
-- | Attempt to fix redundant import warning.
-- Returns 'Nothing' if incapable of fixing.
fixRedundancyWarning :: Int
-> RedundancyWarn
-> [BS.ByteString]
-> Maybe [BS.ByteString]
fixRedundancyWarning startLine warn srcLines =
-- The span for redundant errors is only ever a single line. This means we
-- must search for the end of the import statement. If this a warning about a
-- single import thing, the span line may not encompass the start of the
-- import statement so we must search for that as well.
let (before, stmt : after) = splitAt (startLine - 1) srcLines
isStart bs = (== "import") . BS.take 6 $ BS.dropSpace bs
(before', stmt')
| isStart stmt = (before, [stmt])
| otherwise =
let (inS, st : rs) = break isStart $ stmt : reverse before
in (reverse rs, st : reverse inS)
(stmt'', after') = splitAtImportEnd $ stmt' <> after
in case warn of
WholeModule ->
Just $ before <> after
IndividualThings things ->
(<> after') . (before' <>) . BS.lines <$>
foldM fixRedundantThing
(BS.unlines stmt'')
things
splitAtImportEnd :: [BS.ByteString] -> ([BS.ByteString], [BS.ByteString])
splitAtImportEnd ls = first reverse $ go 0 0 ([], ls) where
go o c acc
| o /= 0 , o == c
= acc
go _ _ acc@(_, []) = acc -- shouldn't happen
go o c (stmt, r:rest) =
let addO = length $ BS.elemIndices '(' r
addC = length $ BS.elemIndices ')' r
in go (o + addO) (c + addC) (r : stmt, rest)
-- | Removes a particular thing from an import list without disrupting the
-- formatting. Returns 'Nothing' if the thing doesn't exist or appears more
-- than once.
--
-- Edges cases not handled:
-- - Comments interspersed in the statement that mention the thing
-- - Semicolon layout
fixRedundantThing :: BS.ByteString -> String -> Maybe BS.ByteString
fixRedundantThing stmt thing
| (start, match) <- BS.breakSubstring thingBS stmt
, not (BS.null match)
, isSeparator . BS.take 1 $ BS.drop thingLen match
, isCellStart . BS.take 1 $ BS.reverse start
-- check that there isn't a second match
, (start2, match2) <- BS.breakSubstring thingBS (BS.drop thingLen match)
, BS.null match2
|| not
( isSeparator (BS.take 1 $ BS.drop thingLen match2)
&& isCellStart (BS.take 1 $ BS.reverse start2)
)
, let start' = BS.dropWhileEnd (\c -> c /= ',' && c /= '(') start
end = dropRest
. BS.dropSpace
$ BS.drop thingLen match
= case BS.take 1 end of
"," -> Just $ start' <> BS.drop 1 end
")" -> Just $ BS.take (BS.length start' - 1) start' <> end
_ -> Nothing
| otherwise = Nothing
where
thingBS = BS.pack thing
thingLen = BS.length thingBS
isSeparator c = BS.all isSpace c || c `elem` [",", "(", ")"]
isCellStart c = BS.all isSpace c || c `elem` [",", "("]
dropRest bs = case BS.uncons bs of
Nothing -> ""
-- Constructors of a type or methods of a class
Just (c, r)
| c == '(' -> BS.dropWhile (\x -> x /= ',' && x /= ')')
. BS.drop 1
$ BS.dropWhile (/= ')') r
_ -> BS.dropSpace bs
--------------------------------------------------------------------------------
-- Parsing
--------------------------------------------------------------------------------
-- | Redundant import warnings
data RedundancyWarn
= WholeModule
| IndividualThings [String]
deriving Show
parseRedundancyWarn :: Warning -> Maybe RedundancyWarn
parseRedundancyWarn (Warning warn) =
case P.readP_to_S redundancyWarnParser (show warn) of
[(w, "")] -> Just w
_ -> Nothing
redundancyWarnParser :: P.ReadP RedundancyWarn
redundancyWarnParser = do
_ <- P.string "The import of ‘"
<|> P.string "The qualified import of ‘"
inQuotes <-
P.sepBy1 (P.munch1 $ \c -> not (isSpace c) && c /= ',' && c /= '’')
(P.char ',' <* P.skipSpaces)
_ <- P.char '’'
let terms
= IndividualThings inQuotes
<$ ( P.skipSpaces
*> P.string "from module ‘"
*> P.munch1 (/= '’')
*> P.string "’ is redundant"
)
wholeMod = WholeModule
<$ (P.skipSpaces *> P.string "is redundant")
result <- P.choice [terms, wholeMod]
_ <- P.munch (const True)
pure result