pinned-warnings-0.1.0.12: src/Internal/FixWarnings.hs
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Internal.FixWarnings
( fixWarning
, fixRedundancyWarning
, RedundancyWarn(..)
) 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.Maybe (isJust)
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
fixWarning :: ModuleFile -> WarningsWithModDate -> IO WarningsWithModDate
fixWarning modFile
warns@MkWarningsWithModDate
{ lastUpdated = modifiedAt
, warningsMap = MonoidMap warnMap
} = do
lastModification <- liftIO $ Dir.getModificationTime modFile
-- Do not attempt to edit if file has been touched since last reload
if lastModification /= modifiedAt
then do
putStrLn
$ "'" <> modFile
<> "' has been modified since last compiled. Reload and try again."
pure warns
else do
curSrcLines <- liftIO . fmap BS.lines $ BS.readFile modFile
-- State is used to keep the contents of the source file in memory while
-- warnings for the file are fixed.
(pairs, newFileContents) <- (`runStateT` curSrcLines)
. flip filterM (reverse $ M.toList warnMap) $ \case
((start, _), warnSet)
| Alt (Just reWarn) -- Take the first redundancy warning parsed
<- foldMap (Alt . parseRedundancyWarn) warnSet
-> do
srcLines <- get
-- attempt to fix the warning
let startLine = Ghc.srcLocLine start
mNewSrcLines =
fixRedundancyWarning startLine reWarn srcLines
case mNewSrcLines of
Nothing -> pure True
Just newSrcLines -> do
put newSrcLines
pure False
_ -> pure True
when (length pairs /= length warnMap) $ do
-- write the changes to the file
BS.writeFile modFile $ BS.unlines newFileContents
putStrLn $ "'" <> modFile <> "' has been edited"
pure MkWarningsWithModDate
{ lastUpdated = lastModification
, warningsMap = MonoidMap $ M.fromList pairs
}
-- | Attempt to fix redundant import warning.
-- Returns 'Nothing' if incapable of fixing.
fixRedundancyWarning :: Int
-> RedundancyWarn
-> [BS.ByteString]
-> Maybe [BS.ByteString]
fixRedundancyWarning startLine warn srcLines = do
-- 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.
(before, stmt : after) <- Just $ splitAt (startLine - 1) srcLines
let isStart bs = "import" `BS.isPrefixOf` BS.dropSpace bs
-- If the first line is not the start of the import declaration, search for
-- it in the preceding lines.
(before', stmt') <-
if isStart stmt
then Just (before, [stmt])
else do
(inS, st : rs) <- Just . break isStart $ stmt : reverse before
Just (reverse rs, st : reverse inS)
let (stmt'', after') = splitAtImportEnd $ stmt' <> after
hasExplicitList
-- Check the next line to see if it contains an explicit import list
| a : _ <- after
, BS.length (BS.takeWhile isSpace a)
> BS.length (BS.takeWhile isSpace stmt)
, BS.take 1 (BS.dropSpace a) == "("
= True
| otherwise = isJust (BS.elemIndex '(' stmt)
case warn of
WholeModule
| hasExplicitList -> Just $ before <> after'
| otherwise -> Just $ before <> after
IndividualThings things ->
(<> after') . (before' <>) . BS.lines <$>
foldM fixRedundantThing
(BS.unlines stmt'')
things
-- | Splits at the end of an import with an explicit list by counting the
-- number of opening and closing parens. If the main parens is closed, then
-- that marks the end of the import.
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
-- Bail if there is more than one valid candidate
| [(start, match)] <- filter isValidCandidate $ findCandidates stmt
-- 1) remove the needle
-- 2) remove enclosing parens
-- 3) remove stuff to the right (..) etc.
-- 4) if there's a comma to the right, remove it as well
-- preserve the whitespace immediately after the ',' or '('
, let start' = let (s, e) = BS.breakEnd (`elem` [',', '(']) start
in s <> BS.takeWhile isSpace e
end = BS.drop thingLen match
= do
(start'', end') <- traverse removeAssociatedIds
$ removeEnclosingParens start' end
BS.uncons end' >>= \case
-- Don't do this if the removed thing was an associated constructor
(',', end'')
| Just (_, e) <- BS.unsnoc $ BS.dropWhileEnd isSpace start''
, e `elem` [',', '('] -- Check if the target thing was not an associated constructor/method
-> Just $ start'' <> BS.dropSpace end''
| otherwise -> Just $ start'' <> end'
-- If bound on the right by ')', remove the suffix containing ',' from start
(')', _) -> Just $ BS.init startTrim <> end'
where
startTrim = BS.dropWhileEnd isSpace start''
_ -> Nothing
| otherwise = Nothing
where
thingBS = BS.pack thing
thingLen = BS.length thingBS
-- A list of substring matches where each element is a pair of the prefix
-- with the match and remaining suffix.
findCandidates "" = []
findCandidates inp =
-- first isolate the portion that is within an open parens, otherwise
-- if the module name is the same as the target then the search will fail.
let (beforeParen, inp') = BS.break (\c -> c == '(' || c == ',') inp
(pre, match) = BS.breakSubstring thingBS inp'
in (beforeParen <> pre, match) :
( first ((beforeParen <> pre <> thingBS) <>)
<$> findCandidates (BS.drop thingLen match)
)
-- Test if a match pair is valid by checking that the match is not a
-- substring of a different identifier
isValidCandidate (start, match) =
not (BS.null match)
&& isSeparator (BS.drop thingLen match)
&& isCellStart (BS.reverse start)
isSeparator = headPred $ \c -> isSpace c || c `elem` [',', '(', ')']
isCellStart = headPred $ \c -> isSpace c || c `elem` [',', '(']
headPred :: (Char -> Bool) -> BS.ByteString -> Bool
headPred p = maybe False (p . fst) . BS.uncons
-- If dealing with an operator, there will be enclosing parens with possible
-- whitespace surrounding the operator.
removeEnclosingParens startBS (BS.dropSpace -> endBS)
| Just (')', end') <- BS.uncons endBS
, Just (start', '(') <- BS.unsnoc $ BS.dropWhileEnd isSpace startBS
-- recurse because it could be an associated constructor that is an operator,
-- i.e. NonEmpty((:|))
= removeEnclosingParens start' end'
| otherwise = (startBS, endBS)
-- | Remove list of associated constructors of a type or methods of a class
-- and any space up until the next cell terminator.
removeAssociatedIds :: BS.ByteString -> Maybe BS.ByteString
removeAssociatedIds = checkForParens
where
checkForParens bs =
let bs' = BS.dropSpace bs
in case BS.uncons bs' of
Nothing -> Just ""
Just (c, r)
| c == '(' -> removeParens 1 r
_ -> Just bs'
-- counts the depth of nested parens to handle the case of an operator
-- appearing in the list.
removeParens :: Int -> BS.ByteString -> Maybe BS.ByteString
removeParens 0 bs = Just $ BS.dropSpace bs
removeParens !n bs =
let bs' = BS.dropWhile (\x -> x /= '(' && x /= ')') bs
in case BS.uncons bs' of
Just (c, r)
| c == '(' -> removeParens (succ n) r
| c == ')' -> removeParens (pred n) r
_ -> Nothing
--------------------------------------------------------------------------------
-- Parsing
--------------------------------------------------------------------------------
-- | Redundant import warnings
data RedundancyWarn
= WholeModule
| IndividualThings [String]
deriving Show
parseRedundancyWarn :: Warning -> Maybe RedundancyWarn
parseRedundancyWarn warn =
case P.readP_to_S redundancyWarnParser (showWarning 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 `notElem` [',', '’'])
(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