Created
July 13, 2020 22:24
-
-
Save jneira/70478dee9e9a01d3d10d68218f8fa514 to your computer and use it in GitHub Desktop.
refactor issues
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
PS D:\dev\ws\haskell\hls> hlint .\plugins\hlint-hls-plugin\src\Ide\Plugin\Hlint.hs | |
plugins\hlint-hls-plugin\src\Ide\Plugin\Hlint.hs:8:1-34: Warning: Unused LANGUAGE pragma | |
Found: | |
{-# LANGUAGE RecordWildCards #-} | |
Perhaps you should remove it. | |
Note: may require `{-# LANGUAGE DisambiguateRecordFields #-}` adding to the top of the file | |
plugins\hlint-hls-plugin\src\Ide\Plugin\Hlint.hs:9:1-34: Warning: Unused LANGUAGE pragma | |
Found: | |
{-# LANGUAGE TupleSections #-} | |
Perhaps you should remove it. | |
plugins\hlint-hls-plugin\src\Ide\Plugin\Hlint.hs:11:1-34: Warning: Unused LANGUAGE pragma | |
Found: | |
{-# LANGUAGE ViewPatterns #-} | |
Perhaps you should remove it. | |
plugins\hlint-hls-plugin\src\Ide\Plugin\Hlint.hs:88:12: Suggestion: Redundant $ | |
Found: | |
return $ (diagnostics file ideas, Just ()) | |
Perhaps: | |
return (diagnostics file ideas, Just ()) | |
plugins\hlint-hls-plugin\src\Ide\Plugin\Hlint.hs:206:47-83: Suggestion: Redundant bracket | |
Found: | |
Perhaps: | |
Right . LSP.List . map CACodeAction <$> hlintActions | |
plugins\hlint-hls-plugin\src\Ide\Plugin\Hlint.hs:308:29-43: Suggestion: Redundant bracket | |
Found: | |
Right <$> (return wsEdit) | |
Perhaps: | |
Right <$> return wsEdit | |
6 hints |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{-# LANGUAGE CPP #-} | |
{-# LANGUAGE DeriveAnyClass #-} | |
{-# LANGUAGE DeriveGeneric #-} | |
{-# LANGUAGE DuplicateRecordFields #-} | |
{-# LANGUAGE FlexibleContexts #-} | |
{-# LANGUAGE FlexibleInstances #-} | |
{-# LANGUAGE OverloadedStrings #-} | |
{-# LANGUAGE RecordWildCards #-} | |
{-# LANGUAGE TupleSections #-} | |
{-# LANGUAGE TypeFamilies #-} | |
{-# LANGUAGE ViewPatterns #-} | |
module Ide.Plugin.Hlint | |
( | |
descriptor | |
--, provider | |
) where | |
import Refact.Apply | |
import Control.Arrow ((&&&)) | |
import Control.DeepSeq | |
import Control.Exception | |
import Control.Lens ((^.)) | |
import Control.Monad | |
import Control.Monad.IO.Class | |
import Control.Monad.Trans.Except | |
import Data.Aeson.Types (ToJSON(..), FromJSON(..), Value(..)) | |
import Data.Binary | |
import qualified Data.HashSet as HashSet | |
import Data.Hashable | |
import Data.Maybe | |
import qualified Data.Text as T | |
import qualified Data.Text.IO as T | |
import Data.Typeable | |
import Development.IDE.Core.OfInterest | |
import Development.IDE.Core.Rules | |
import Development.IDE.Core.Shake | |
import Development.IDE.Types.Diagnostics as D | |
import Development.IDE.Types.Location | |
import Development.Shake | |
-- import Development.Shake hiding ( Diagnostic ) | |
import GHC hiding (DynFlags(..)) | |
#ifndef GHC_LIB | |
import GHC (DynFlags(..)) | |
import HscTypes (hsc_dflags) | |
#else | |
import RealGHC (DynFlags(..)) | |
import RealGHC.HscTypes (hsc_dflags) | |
import qualified RealGHC.EnumSet as EnumSet | |
import Language.Haskell.GhclibParserEx.GHC.Driver.Session as GhclibParserEx (readExtension) | |
#endif | |
import Ide.Logger | |
import Ide.Types | |
import Ide.Plugin | |
import Ide.PluginUtils | |
import Language.Haskell.HLint as Hlint | |
import Language.Haskell.LSP.Types | |
import qualified Language.Haskell.LSP.Types as LSP | |
import qualified Language.Haskell.LSP.Types.Lens as LSP | |
import Text.Regex.TDFA.Text() | |
import GHC.Generics (Generic) | |
-- --------------------------------------------------------------------- | |
descriptor :: PluginId -> PluginDescriptor | |
descriptor plId = (defaultPluginDescriptor plId) | |
{ pluginRules = rules | |
, pluginCommands = | |
[ PluginCommand "applyOne" "Apply a single hint" applyOneCmd | |
, PluginCommand "applyAll" "Apply all hints to the file" applyAllCmd | |
] | |
, pluginCodeActionProvider = Just codeActionProvider | |
} | |
data GetHlintDiagnostics = GetHlintDiagnostics | |
deriving (Eq, Show, Typeable, Generic) | |
instance Hashable GetHlintDiagnostics | |
instance NFData GetHlintDiagnostics | |
instance Binary GetHlintDiagnostics | |
type instance RuleResult GetHlintDiagnostics = () | |
rules :: Rules () | |
rules = do | |
define $ \GetHlintDiagnostics file -> do | |
ideas <- getIdeas file | |
return $ (diagnostics file ideas, Just ()) | |
getHlintSettingsRule (HlintEnabled []) | |
action $ do | |
files <- getFilesOfInterest | |
void $ uses GetHlintDiagnostics $ HashSet.toList files | |
where | |
diagnostics :: NormalizedFilePath -> Either ParseError [Idea] -> [FileDiagnostic] | |
diagnostics file (Right ideas) = | |
[(file, ShowDiag, ideaToDiagnostic i) | i <- ideas, ideaSeverity i /= Ignore] | |
diagnostics file (Left parseErr) = | |
[(file, ShowDiag, parseErrorToDiagnostic parseErr)] | |
ideaToDiagnostic :: Idea -> Diagnostic | |
ideaToDiagnostic idea = | |
LSP.Diagnostic { | |
_range = srcSpanToRange $ ideaSpan idea | |
, _severity = Just LSP.DsInfo | |
, _code = Just (LSP.StringValue $ T.pack $ ideaHint idea) | |
, _source = Just "hlint" | |
, _message = T.pack $ show idea | |
, _relatedInformation = Nothing | |
, _tags = Nothing | |
} | |
parseErrorToDiagnostic :: ParseError -> Diagnostic | |
parseErrorToDiagnostic (Hlint.ParseError l msg contents) = | |
LSP.Diagnostic { | |
_range = srcSpanToRange l | |
, _severity = Just LSP.DsInfo | |
, _code = Just (LSP.StringValue "parser") | |
, _source = Just "hlint" | |
, _message = T.unlines [T.pack msg,T.pack contents] | |
, _relatedInformation = Nothing | |
, _tags = Nothing | |
} | |
-- This one is defined in Development.IDE.GHC.Error but here | |
-- the types could come from ghc-lib or ghc | |
srcSpanToRange :: SrcSpan -> LSP.Range | |
srcSpanToRange (RealSrcSpan span) = Range { | |
_start = LSP.Position { | |
_line = srcSpanStartLine span - 1 | |
, _character = srcSpanStartCol span - 1} | |
, _end = LSP.Position { | |
_line = srcSpanEndLine span - 1 | |
, _character = srcSpanEndCol span - 1} | |
} | |
srcSpanToRange (UnhelpfulSpan _) = noRange | |
getIdeas :: NormalizedFilePath -> Action (Either ParseError [Idea]) | |
getIdeas nfp = do | |
logm $ "getIdeas:file:" ++ show nfp | |
(flags, classify, hint) <- useNoFile_ GetHlintSettings | |
let applyHints' (Just (Right modEx)) = Right $ applyHints classify hint [modEx] | |
applyHints' (Just (Left err)) = Left err | |
applyHints' Nothing = Right [] | |
fmap applyHints' (moduleEx flags) | |
where moduleEx :: ParseFlags -> Action (Maybe (Either ParseError ModuleEx)) | |
#ifndef GHC_LIB | |
moduleEx _flags = do | |
mbpm <- getParsedModule nfp | |
case mbpm of | |
Nothing -> return Nothing | |
Just pm -> do | |
let anns = pm_annotations pm | |
let modu = pm_parsed_source pm | |
return $ Just $ Right (createModuleEx anns modu) | |
#else | |
moduleEx flags = do | |
flags' <- setExtensions flags | |
Just <$> (liftIO $ parseModuleEx flags' (fromNormalizedFilePath nfp) Nothing) | |
setExtensions flags = do | |
hsc <- hscEnv <$> use_ GhcSession nfp | |
let dflags = hsc_dflags hsc | |
let hscExts = EnumSet.toList (extensionFlags dflags) | |
logm $ "getIdeas:setExtensions:hscExtensions:" ++ show hscExts | |
let hlintExts = mapMaybe (GhclibParserEx.readExtension . show) hscExts | |
logm $ "getIdeas:setExtensions:hlintExtensions:" ++ show hlintExts | |
return $ flags { enabledExtensions = hlintExts } | |
#endif | |
-- --------------------------------------------------------------------- | |
data HlintUsage | |
= HlintEnabled { cmdArgs :: [String] } | |
| HlintDisabled | |
deriving Show | |
data GetHlintSettings = GetHlintSettings | |
deriving (Eq, Show, Typeable, Generic) | |
instance Hashable GetHlintSettings | |
instance NFData GetHlintSettings | |
instance NFData Hint where rnf = rwhnf | |
instance NFData Classify where rnf = rwhnf | |
instance NFData ParseFlags where rnf = rwhnf | |
instance Show Hint where show = const "<hint>" | |
instance Show ParseFlags where show = const "<parseFlags>" | |
instance Binary GetHlintSettings | |
type instance RuleResult GetHlintSettings = (ParseFlags, [Classify], Hint) | |
getHlintSettingsRule :: HlintUsage -> Rules () | |
getHlintSettingsRule usage = | |
defineNoFile $ \GetHlintSettings -> | |
liftIO $ case usage of | |
HlintEnabled cmdArgs -> argsSettings cmdArgs | |
HlintDisabled -> fail "hlint configuration unspecified" | |
-- --------------------------------------------------------------------- | |
codeActionProvider :: CodeActionProvider | |
codeActionProvider _ _ plId docId _ context = (Right . LSP.List . map CACodeAction) <$> hlintActions | |
where | |
hlintActions :: IO [LSP.CodeAction] | |
hlintActions = catMaybes <$> mapM mkHlintAction (filter validCommand diags) | |
-- |Some hints do not have an associated refactoring | |
validCommand (LSP.Diagnostic _ _ (Just (LSP.StringValue code)) (Just "hlint") _ _ _) = | |
code /= "Eta reduce" | |
validCommand _ = False | |
LSP.List diags = context ^. LSP.diagnostics | |
mkHlintAction :: LSP.Diagnostic -> IO (Maybe LSP.CodeAction) | |
mkHlintAction diag@(LSP.Diagnostic (LSP.Range start _) _s (Just (LSP.StringValue code)) (Just "hlint") m _ _) = | |
Just . codeAction <$> mkLspCommand plId "applyOne" title (Just args) | |
where | |
codeAction cmd = LSP.CodeAction title (Just LSP.CodeActionQuickFix) (Just (LSP.List [diag])) Nothing (Just cmd) | |
title = "Apply hint:" <> head (T.lines m) | |
-- need 'file', 'start_pos' and hint title (to distinguish between alternative suggestions at the same location) | |
args = [toJSON (AOP (docId ^. LSP.uri) start code)] | |
mkHlintAction (LSP.Diagnostic _r _s _c _source _m _ _) = return Nothing | |
-- --------------------------------------------------------------------- | |
applyAllCmd :: CommandFunction Uri | |
applyAllCmd _lf ide uri = do | |
let file = maybe (error $ show uri ++ " is not a file.") | |
toNormalizedFilePath' | |
(uriToFilePath' uri) | |
logm $ "applyAllCmd:file=" ++ show file | |
res <- applyHint ide file Nothing | |
logm $ "applyAllCmd:res=" ++ show res | |
return $ | |
case res of | |
Left err -> (Left (responseError (T.pack $ "applyAll: " ++ show err)), Nothing) | |
Right fs -> (Right Null, Just (WorkspaceApplyEdit, ApplyWorkspaceEditParams fs)) | |
-- --------------------------------------------------------------------- | |
data ApplyOneParams = AOP | |
{ file :: Uri | |
, start_pos :: Position | |
-- | There can be more than one hint suggested at the same position, so HintTitle is used to distinguish between them. | |
, hintTitle :: HintTitle | |
} deriving (Eq,Show,Generic,FromJSON,ToJSON) | |
type HintTitle = T.Text | |
data OneHint = OneHint | |
{ oneHintPos :: Position | |
, oneHintTitle :: HintTitle | |
} deriving (Eq, Show) | |
applyOneCmd :: CommandFunction ApplyOneParams | |
applyOneCmd _lf ide (AOP uri pos title) = do | |
let oneHint = OneHint pos title | |
let file = maybe (error $ show uri ++ " is not a file.") toNormalizedFilePath' | |
(uriToFilePath' uri) | |
res <- applyHint ide file (Just oneHint) | |
logm $ "applyOneCmd:file=" ++ show file | |
logm $ "applyOneCmd:res=" ++ show res | |
return $ | |
case res of | |
Left err -> (Left (responseError (T.pack $ "applyOne: " ++ show err)), Nothing) | |
Right fs -> (Right Null, Just (WorkspaceApplyEdit, ApplyWorkspaceEditParams fs)) | |
applyHint :: IdeState -> NormalizedFilePath -> Maybe OneHint -> IO (Either String WorkspaceEdit) | |
applyHint ide nfp mhint = | |
runExceptT $ do | |
ideas <- bimapExceptT showParseError id $ ExceptT $ liftIO $ runAction "applyHint" ide $ getIdeas nfp | |
let ideas' = maybe ideas (`filterIdeas` ideas) mhint | |
let commands = map (show &&& ideaRefactoring) ideas' | |
liftIO $ logm $ "applyHint:apply=" ++ show commands | |
-- set Nothing as "position" for "applyRefactorings" because | |
-- applyRefactorings expects the provided position to be _within_ the scope | |
-- of each refactoring it will apply. | |
-- But "Idea"s returned by HLint point to starting position of the expressions | |
-- that contain refactorings, so they are often outside the refactorings' boundaries. | |
-- Example: | |
-- Given an expression "hlintTest = reid $ (myid ())" | |
-- Hlint returns an idea at the position (1,13) | |
-- That contains "Redundant brackets" refactoring at position (1,20): | |
-- | |
-- [("src/App/Test.hs:5:13: Warning: Redundant bracket\nFound:\n reid $ (myid ())\nWhy not:\n reid $ myid ()\n",[Replace {rtype = Expr, pos = SrcSpan {startLine = 5, startCol = 20, endLine = 5, endCol = 29}, subts = [("x",SrcSpan {startLine = 5, startCol = 21, endLine = 5, endCol = 28})], orig = "x"}])] | |
-- | |
-- If we provide "applyRefactorings" with "Just (1,13)" then | |
-- the "Redundant bracket" hint will never be executed | |
-- because SrcSpan (1,20,??,??) doesn't contain position (1,13). | |
let fp = fromNormalizedFilePath nfp | |
res <- liftIO $ (Right <$> applyRefactorings Nothing commands fp) `catches` | |
[ Handler $ \e -> return (Left (show (e :: IOException))) | |
, Handler $ \e -> return (Left (show (e :: ErrorCall))) | |
] | |
case res of | |
Right appliedFile -> do | |
let uri = fromNormalizedUri (filePathToUri' nfp) | |
oldContent <- liftIO $ T.readFile fp | |
liftIO $ logm $ "applyHint:oldContent=" ++ show oldContent | |
liftIO $ logm $ "applyHint:appliedFile=" ++ show (T.pack appliedFile) | |
let wsEdit = diffText' True (uri, oldContent) (T.pack appliedFile) IncludeDeletions | |
liftIO $ logm $ "applyHint:diff=" ++ show wsEdit | |
ExceptT $ Right <$> (return wsEdit) | |
Left err -> | |
throwE (show err) | |
where | |
-- | If we are only interested in applying a particular hint then | |
-- let's filter out all the irrelevant ideas | |
filterIdeas :: OneHint -> [Idea] -> [Idea] | |
filterIdeas (OneHint (Position l c) title) ideas = | |
let title' = T.unpack title | |
ideaPos = (srcSpanStartLine &&& srcSpanStartCol) . toRealSrcSpan . ideaSpan | |
in filter (\i -> ideaHint i == title' && ideaPos i == (l+1, c+1)) ideas | |
toRealSrcSpan (RealSrcSpan real) = real | |
toRealSrcSpan (UnhelpfulSpan x) = error $ "No real source span: " ++ show x | |
showParseError :: Hlint.ParseError -> String | |
showParseError (Hlint.ParseError location message content) = | |
unlines [show location, message, content] | |
-- | Map over both failure and success. | |
bimapExceptT :: Functor m => (e -> f) -> (a -> b) -> ExceptT e m a -> ExceptT f m b | |
bimapExceptT f g (ExceptT m) = ExceptT (fmap h m) where | |
h (Left e) = Left (f e) | |
h (Right a) = Right (g a) | |
{-# INLINE bimapExceptT #-} | |
-- --------------------------------------------------------------------- | |
{- | |
{-# LANGUAGE CPP #-} | |
{-# LANGUAGE DeriveAnyClass #-} | |
{-# LANGUAGE DeriveGeneric #-} | |
{-# LANGUAGE DuplicateRecordFields #-} | |
{-# LANGUAGE OverloadedStrings #-} | |
-- | apply-refact applies refactorings specified by the refact package. It is | |
-- currently integrated into hlint to enable the automatic application of | |
-- suggestions. | |
module Haskell.Ide.Engine.Plugin.ApplyRefact where | |
import Control.Arrow | |
import Control.Exception ( IOException | |
, ErrorCall | |
, Handler(..) | |
, catches | |
, try | |
) | |
import Control.Lens hiding ( List ) | |
import Control.Monad.IO.Class | |
import Control.Monad.Trans.Except | |
import Data.Aeson hiding (Error) | |
import Data.Maybe | |
#if __GLASGOW_HASKELL__ < 808 | |
import Data.Monoid ((<>)) | |
#endif | |
import qualified Data.Text as T | |
import GHC.Generics | |
import Haskell.Ide.Engine.MonadFunctions | |
import Haskell.Ide.Engine.MonadTypes | |
import Haskell.Ide.Engine.PluginUtils | |
import Language.Haskell.Exts.SrcLoc | |
import Language.Haskell.Exts.Parser | |
import Language.Haskell.Exts.Extension | |
import Language.Haskell.HLint4 as Hlint | |
import qualified Language.Haskell.LSP.Types as LSP | |
import qualified Language.Haskell.LSP.Types.Lens as LSP | |
import Refact.Apply | |
-- --------------------------------------------------------------------- | |
{-# ANN module ("HLint: ignore Eta reduce" :: String) #-} | |
{-# ANN module ("HLint: ignore Redundant do" :: String) #-} | |
{-# ANN module ("HLint: ignore Reduce duplication" :: String) #-} | |
-- --------------------------------------------------------------------- | |
type HintTitle = T.Text | |
applyRefactDescriptor :: PluginId -> PluginDescriptor | |
applyRefactDescriptor plId = PluginDescriptor | |
{ pluginId = plId | |
, pluginName = "ApplyRefact" | |
, pluginDesc = "apply-refact applies refactorings specified by the refact package. It is currently integrated into hlint to enable the automatic application of suggestions." | |
, pluginCommands = | |
[ PluginCommand "applyOne" "Apply a single hint" applyOneCmd | |
, PluginCommand "applyAll" "Apply all hints to the file" applyAllCmd | |
] | |
, pluginCodeActionProvider = Just codeActionProvider | |
, pluginDiagnosticProvider = Nothing | |
, pluginHoverProvider = Nothing | |
, pluginSymbolProvider = Nothing | |
, pluginFormattingProvider = Nothing | |
} | |
-- --------------------------------------------------------------------- | |
data ApplyOneParams = AOP | |
{ file :: Uri | |
, start_pos :: Position | |
-- | There can be more than one hint suggested at the same position, so HintTitle is used to distinguish between them. | |
, hintTitle :: HintTitle | |
} deriving (Eq,Show,Generic,FromJSON,ToJSON) | |
data OneHint = OneHint | |
{ oneHintPos :: Position | |
, oneHintTitle :: HintTitle | |
} deriving (Eq, Show) | |
applyOneCmd :: ApplyOneParams -> IdeGhcM (IdeResult WorkspaceEdit) | |
applyOneCmd (AOP uri pos title) = pluginGetFile "applyOne: " uri $ \fp -> do | |
let oneHint = OneHint pos title | |
revMapp <- reverseFileMap | |
let defaultResult = do | |
debugm "applyOne: no access to the persisted file." | |
return $ IdeResultOk mempty | |
withMappedFile fp defaultResult $ \file' -> do | |
res <- liftToGhc $ applyHint file' (Just oneHint) revMapp | |
logm $ "applyOneCmd:file=" ++ show fp | |
logm $ "applyOneCmd:res=" ++ show res | |
case res of | |
Left err -> return $ IdeResultFail | |
(IdeError PluginError (T.pack $ "applyOne: " ++ show err) Null) | |
Right fs -> return (IdeResultOk fs) | |
-- --------------------------------------------------------------------- | |
applyAllCmd :: Uri -> IdeGhcM (IdeResult WorkspaceEdit) | |
applyAllCmd uri = pluginGetFile "applyAll: " uri $ \fp -> do | |
let defaultResult = do | |
debugm "applyAll: no access to the persisted file." | |
return $ IdeResultOk mempty | |
revMapp <- reverseFileMap | |
withMappedFile fp defaultResult $ \file' -> do | |
res <- liftToGhc $ applyHint file' Nothing revMapp | |
logm $ "applyAllCmd:res=" ++ show res | |
case res of | |
Left err -> return $ IdeResultFail (IdeError PluginError | |
(T.pack $ "applyAll: " ++ show err) Null) | |
Right fs -> return (IdeResultOk fs) | |
-- --------------------------------------------------------------------- | |
-- AZ:TODO: Why is this in IdeGhcM? | |
lint :: Uri -> IdeGhcM (IdeResult PublishDiagnosticsParams) | |
lint uri = pluginGetFile "lint: " uri $ \fp -> do | |
let | |
defaultResult = do | |
debugm "lint: no access to the persisted file." | |
return | |
$ IdeResultOk (PublishDiagnosticsParams (filePathToUri fp) $ List []) | |
withMappedFile fp defaultResult $ \file' -> do | |
eitherErrorResult <- liftIO | |
(try $ runExceptT $ runLint file' [] :: IO | |
(Either IOException (Either [Diagnostic] [Idea])) | |
) | |
case eitherErrorResult of | |
Left err -> return $ IdeResultFail | |
(IdeError PluginError (T.pack $ "lint: " ++ show err) Null) | |
Right res -> case res of | |
Left diags -> | |
return | |
(IdeResultOk | |
(PublishDiagnosticsParams (filePathToUri fp) $ List diags) | |
) | |
Right fs -> | |
return | |
$ IdeResultOk | |
$ PublishDiagnosticsParams (filePathToUri fp) | |
$ List (map hintToDiagnostic $ stripIgnores fs) | |
runLint :: FilePath -> [String] -> ExceptT [Diagnostic] IO [Idea] | |
runLint fp args = do | |
(flags,classify,hint) <- liftIO $ argsSettings args | |
let myflags = flags { hseFlags = (hseFlags flags) { extensions = EnableExtension TypeApplications:extensions (hseFlags flags)}} | |
res <- bimapExceptT parseErrorToDiagnostic id $ ExceptT $ parseModuleEx myflags fp Nothing | |
pure $ applyHints classify hint [res] | |
parseErrorToDiagnostic :: Hlint.ParseError -> [Diagnostic] | |
parseErrorToDiagnostic (Hlint.ParseError l msg contents) = | |
[Diagnostic | |
{ _range = srcLoc2Range l | |
, _severity = Just DsInfo -- Not displayed | |
, _code = Just (LSP.StringValue "parser") | |
, _source = Just "hlint" | |
, _message = T.unlines [T.pack msg,T.pack contents] | |
, _relatedInformation = Nothing | |
}] | |
{- | |
-- | An idea suggest by a 'Hint'. | |
data Idea = Idea | |
{ideaModule :: String -- ^ The module the idea applies to, may be @\"\"@ if the module cannot be determined or is a result of cross-module hints. | |
,ideaDecl :: String -- ^ The declaration the idea applies to, typically the function name, but may be a type name. | |
,ideaSeverity :: Severity -- ^ The severity of the idea, e.g. 'Warning'. | |
,ideaHint :: String -- ^ The name of the hint that generated the idea, e.g. @\"Use reverse\"@. | |
,ideaSpan :: SrcSpan -- ^ The source code the idea relates to. | |
,ideaFrom :: String -- ^ The contents of the source code the idea relates to. | |
,ideaTo :: Maybe String -- ^ The suggested replacement, or 'Nothing' for no replacement (e.g. on parse errors). | |
,ideaNote :: [Note] -- ^ Notes about the effect of applying the replacement. | |
,ideaRefactoring :: [Refactoring R.SrcSpan] -- ^ How to perform this idea | |
} | |
deriving (Eq,Ord) | |
-} | |
-- | Map over both failure and success. | |
bimapExceptT :: Functor m => (e -> f) -> (a -> b) -> ExceptT e m a -> ExceptT f m b | |
bimapExceptT f g (ExceptT m) = ExceptT (fmap h m) where | |
h (Left e) = Left (f e) | |
h (Right a) = Right (g a) | |
{-# INLINE bimapExceptT #-} | |
-- --------------------------------------------------------------------- | |
stripIgnores :: [Idea] -> [Idea] | |
stripIgnores ideas = filter notIgnored ideas | |
where | |
notIgnored idea = ideaSeverity idea /= Ignore | |
-- --------------------------------------------------------------------- | |
hintToDiagnostic :: Idea -> Diagnostic | |
hintToDiagnostic idea | |
= Diagnostic | |
{ _range = ss2Range (ideaSpan idea) | |
, _severity = Just (hintSeverityMap $ ideaSeverity idea) | |
, _code = Just (LSP.StringValue $ T.pack $ ideaHint idea) | |
, _source = Just "hlint" | |
, _message = idea2Message idea | |
, _relatedInformation = Nothing | |
} | |
-- --------------------------------------------------------------------- | |
idea2Message :: Idea -> T.Text | |
idea2Message idea = T.unlines $ [T.pack $ ideaHint idea, "Found:", " " <> T.pack (ideaFrom idea)] | |
<> toIdea <> map (T.pack . show) (ideaNote idea) | |
where | |
toIdea :: [T.Text] | |
toIdea = case ideaTo idea of | |
Nothing -> [] | |
Just i -> [T.pack "Why not:", T.pack $ " " ++ i] | |
-- --------------------------------------------------------------------- | |
-- | Maps hlint severities to LSP severities | |
-- | We want to lower the severities so HLint errors and warnings | |
-- | don't mix with GHC errors and warnings: | |
-- | as per https://github.com/haskell/haskell-ide-engine/issues/375 | |
hintSeverityMap :: Severity -> DiagnosticSeverity | |
hintSeverityMap Ignore = DsInfo -- cannot really happen after stripIgnores | |
hintSeverityMap Suggestion = DsHint | |
hintSeverityMap Warning = DsInfo | |
hintSeverityMap Error = DsInfo | |
-- --------------------------------------------------------------------- | |
srcLoc2Range :: SrcLoc -> Range | |
srcLoc2Range (SrcLoc _ l c) = Range ps pe | |
where | |
ps = Position (l-1) (c-1) | |
pe = Position (l-1) 100000 | |
-- --------------------------------------------------------------------- | |
ss2Range :: SrcSpan -> Range | |
ss2Range ss = Range ps pe | |
where | |
ps = Position (srcSpanStartLine ss - 1) (srcSpanStartColumn ss - 1) | |
pe = Position (srcSpanEndLine ss - 1) (srcSpanEndColumn ss - 1) | |
-- --------------------------------------------------------------------- | |
applyHint :: FilePath -> Maybe OneHint -> (FilePath -> FilePath) -> IdeM (Either String WorkspaceEdit) | |
applyHint fp mhint fileMap = do | |
runExceptT $ do | |
ideas <- getIdeas fp mhint | |
let commands = map (show &&& ideaRefactoring) ideas | |
liftIO $ logm $ "applyHint:apply=" ++ show commands | |
-- set Nothing as "position" for "applyRefactorings" because | |
-- applyRefactorings expects the provided position to be _within_ the scope | |
-- of each refactoring it will apply. | |
-- But "Idea"s returned by HLint pont to starting position of the expressions | |
-- that contain refactorings, so they are often outside the refactorings' boundaries. | |
-- Example: | |
-- Given an expression "hlintTest = reid $ (myid ())" | |
-- Hlint returns an idea at the position (1,13) | |
-- That contains "Redundant brackets" refactoring at position (1,20): | |
-- | |
-- [("src/App/Test.hs:5:13: Warning: Redundant bracket\nFound:\n reid $ (myid ())\nWhy not:\n reid $ myid ()\n",[Replace {rtype = Expr, pos = SrcSpan {startLine = 5, startCol = 20, endLine = 5, endCol = 29}, subts = [("x",SrcSpan {startLine = 5, startCol = 21, endLine = 5, endCol = 28})], orig = "x"}])] | |
-- | |
-- If we provide "applyRefactorings" with "Just (1,13)" then | |
-- the "Redundant bracket" hint will never be executed | |
-- because SrcSpan (1,20,??,??) doesn't contain position (1,13). | |
res <- liftIO $ (Right <$> applyRefactorings Nothing commands fp) `catches` | |
[ Handler $ \e -> return (Left (show (e :: IOException))) | |
, Handler $ \e -> return (Left (show (e :: ErrorCall))) | |
] | |
case res of | |
Right appliedFile -> do | |
diff <- ExceptT $ Right <$> makeDiffResult fp (T.pack appliedFile) fileMap | |
liftIO $ logm $ "applyHint:diff=" ++ show diff | |
return diff | |
Left err -> | |
throwE (show err) | |
-- | Gets HLint ideas for | |
getIdeas :: MonadIO m => FilePath -> Maybe OneHint -> ExceptT String m [Idea] | |
getIdeas lintFile mhint = do | |
let hOpts = hlintOpts lintFile (oneHintPos <$> mhint) | |
ideas <- runHlint lintFile hOpts | |
pure $ maybe ideas (`filterIdeas` ideas) mhint | |
-- | If we are only interested in applying a particular hint then | |
-- let's filter out all the irrelevant ideas | |
filterIdeas :: OneHint -> [Idea] -> [Idea] | |
filterIdeas (OneHint (Position l c) title) ideas = | |
let | |
title' = T.unpack title | |
ideaPos = (srcSpanStartLine &&& srcSpanStartColumn) . ideaSpan | |
in filter (\i -> ideaHint i == title' && ideaPos i == (l+1, c+1)) ideas | |
hlintOpts :: FilePath -> Maybe Position -> [String] | |
hlintOpts lintFile mpos = | |
let | |
posOpt (Position l c) = " --pos " ++ show (l+1) ++ "," ++ show (c+1) | |
opts = maybe "" posOpt mpos | |
in [lintFile, "--quiet", "--refactor", "--refactor-options=" ++ opts ] | |
runHlint :: MonadIO m => FilePath -> [String] -> ExceptT String m [Idea] | |
runHlint fp args = | |
do (flags,classify,hint) <- liftIO $ argsSettings args | |
let myflags = flags { hseFlags = (hseFlags flags) { extensions = EnableExtension TypeApplications:extensions (hseFlags flags)}} | |
res <- bimapExceptT showParseError id $ ExceptT $ liftIO $ parseModuleEx myflags fp Nothing | |
pure $ applyHints classify hint [res] | |
showParseError :: Hlint.ParseError -> String | |
showParseError (Hlint.ParseError location message content) = | |
unlines [show location, message, content] | |
-- --------------------------------------------------------------------- | |
codeActionProvider :: CodeActionProvider | |
codeActionProvider plId docId _ context = IdeResultOk <$> hlintActions | |
where | |
hlintActions :: IdeM [LSP.CodeAction] | |
hlintActions = catMaybes <$> mapM mkHlintAction (filter validCommand diags) | |
-- |Some hints do not have an associated refactoring | |
validCommand (LSP.Diagnostic _ _ (Just (LSP.StringValue code)) (Just "hlint") _ _) = | |
case code of | |
"Eta reduce" -> False | |
_ -> True | |
validCommand _ = False | |
LSP.List diags = context ^. LSP.diagnostics | |
mkHlintAction :: LSP.Diagnostic -> IdeM (Maybe LSP.CodeAction) | |
mkHlintAction diag@(LSP.Diagnostic (LSP.Range start _) _s (Just (LSP.StringValue code)) (Just "hlint") m _) = | |
Just . codeAction <$> mkLspCommand plId "applyOne" title (Just args) | |
where | |
codeAction cmd = LSP.CodeAction title (Just LSP.CodeActionQuickFix) (Just (LSP.List [diag])) Nothing (Just cmd) | |
title = "Apply hint:" <> head (T.lines m) | |
-- need 'file', 'start_pos' and hint title (to distinguish between alternative suggestions at the same location) | |
args = [toJSON (AOP (docId ^. LSP.uri) start code)] | |
mkHlintAction (LSP.Diagnostic _r _s _c _source _m _) = return Nothing | |
-} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{-# LANGUAGE CPP #-} | |
{-# LANGUAGE DeriveAnyClass #-} | |
{-# LANGUAGE DeriveGeneric #-} | |
{-# LANGUAGE DuplicateRecordFields #-} | |
{-# LANGUAGE FlexibleContexts #-} | |
{-# LANGUAGE FlexibleInstances #-} | |
{-# LANGUAGE OverloadedStrings #-} | |
{-# LANGUAGE TypeFamilies #-} | |
{-# LANGUAGE DeriveAnyClass #-} | |
{-# LANGUAGE DeriveGeneric #-} | |
{-# LANGUAGE DuplicateRecordFields #-} | |
{-# LANGUAGE FlexibleContexts #-} | |
{-# LANGUAGE FlexibleInstances #-} | |
{-# LANGUAGE OverloadedStrings #-} | |
{-# LANGUAGE TypeFamilies #-} | |
modulemoduleIde.Plugin.HlintIde.Plugin.Hlint | |
(( | |
descriptor | |
--, provider | |
--, provider | |
))wherewhere | |
importimport Refact.ApplyRefact.Apply | |
importimport Control.ArrowControl.Arrow ((((&&&&&&)))) | |
importimport Control.DeepSeqControl.DeepSeq | |
importimport Control.ExceptionControl.Exception | |
importimport Control.LensControl.Lens ((((^.^.)))) | |
importimport Control.MonadControl.Monad | |
importimport Control.Monad.IO.ClassControl.Monad.IO.Class | |
importimport Control.Monad.Trans.ExceptControl.Monad.Trans.Except | |
importimport Data.Aeson.TypesData.Aeson.Types ((ToJSONToJSON((....)),, FromJSONFromJSON((....)),, ValueValue((....)))) | |
importimport Data.BinaryData.Binary | |
importimportqualifiedqualified Data.HashSetData.HashSetasas HashSetHashSet | |
importimport Data.HashableData.Hashable | |
importimport Data.MaybeData.Maybe | |
importimportqualifiedqualified Data.TextData.Textasas TT | |
importimportqualifiedqualified Data.Text.IOData.Text.IOasas TT | |
importimport Data.TypeableData.Typeable | |
importimport Development.IDE.Core.OfInterestDevelopment.IDE.Core.OfInterest | |
importimport Development.IDE.Core.RulesDevelopment.IDE.Core.Rules | |
importimport Development.IDE.Core.ShakeDevelopment.IDE.Core.Shake | |
importimport Development.IDE.Types.DiagnosticsDevelopment.IDE.Types.Diagnosticsasas DD | |
importimport Development.IDE.Types.LocationDevelopment.IDE.Types.Location | |
importimport Development.Shake | |
-- import Development.Shake hiding ( Diagnostic ) | |
-- import Development.Shake hiding ( Diagnostic ) | |
importimport GHCGHC hidinghiding((DynFlagsDynFlags((....)))) | |
#ifndef GHC_LIB | |
importimport GHCGHC ((DynFlagsDynFlags((....)))) | |
importimport HscTypesHscTypes ((hsc_dflagshsc_dflags)) | |
#else | |
import RealGHC (DynFlags(..)) | |
import RealGHC.HscTypes (hsc_dflags) | |
import qualified RealGHC.EnumSet as EnumSet | |
import Language.Haskell.GhclibParserEx.GHC.Driver.Session as GhclibParserEx (readExtension) | |
#endif | |
importimport Ide.LoggerIde.Logger | |
importimport Ide.TypesIde.Types | |
importimport Ide.PluginIde.Plugin | |
importimport Ide.PluginUtilsIde.PluginUtils | |
importimport Language.Haskell.HLintLanguage.Haskell.HLintasas HlintHlint | |
importimport Language.Haskell.LSP.TypesLanguage.Haskell.LSP.Types | |
importimportqualifiedqualified Language.Haskell.LSP.TypesLanguage.Haskell.LSP.Typesasas LSPLSP | |
importimportqualifiedqualified Language.Haskell.LSP.Types.LensLanguage.Haskell.LSP.Types.Lensasas LSPLSP | |
importimport Text.Regex.TDFA.TextText.Regex.TDFA.Text(()) | |
importimport GHC.GenericsGHC.Generics ((GenericGeneric) | |
-- --------------------------------------------------------------------- | |
-- --------------------------------------------------------------------- | |
descriptordescriptor:::: PluginIdPluginId->-> PluginDescriptorPluginDescriptor | |
descriptordescriptor plIdplId== ((defaultPluginDescriptordefaultPluginDescriptor plIdplId)) | |
{{ pluginRulespluginRules== rulesrules | |
,, pluginCommandspluginCommands== | |
[[ PluginCommandPluginCommand "applyOne""applyOne" "Apply a single hint""Apply a single hint" applyOneCmdapplyOneCmd | |
,, PluginCommandPluginCommand "applyAll""applyAll" "Apply all hints to the file""Apply all hints to the file" applyAllCmdapplyAllCmd | |
]] | |
,, pluginCodeActionProviderpluginCodeActionProvider== JustJust codeActionProvidercodeActionProvider | |
}} | |
datadata GetHlintDiagnosticsGetHlintDiagnostics== GetHlintDiagnosticsGetHlintDiagnostics | |
derivingderiving ((EqEq,, ShowShow,, TypeableTypeable,, GenericGeneric)) | |
instanceinstance HashableHashable GetHlintDiagnosticsGetHlintDiagnostics | |
instanceinstance NFDataNFData GetHlintDiagnosticsGetHlintDiagnostics | |
instanceinstance BinaryBinary GetHlintDiagnosticsGetHlintDiagnostics | |
typetypeinstanceinstance RuleResultRuleResult GetHlintDiagnosticsGetHlintDiagnostics== (()) | |
rulesrules:::: RulesRules (()) | |
rulesrules== dodo | |
definedefine $$ \\GetHlintDiagnosticsGetHlintDiagnostics filefile->-> dodo | |
ideasideas<-<- getIdeasgetIdeas filefile | |
returnreturn $$ ((diagnosticsdiagnostics filefile ideasideas,, JustJust (()))) | |
getHlintSettingsRulegetHlintSettingsRule ((HlintEnabledHlintEnabled [[]])) | |
actionaction $$ dodo | |
filesfiles<-<- getFilesOfInterestgetFilesOfInterest | |
voidvoid $$ usesuses GetHlintDiagnosticsGetHlintDiagnostics $$ HashSet.toListHashSet.toList filesfiles | |
wherewhere | |
diagnosticsdiagnostics:::: NormalizedFilePathNormalizedFilePath->-> EitherEither ParseErrorParseError [[IdeaIdea]]->-> [[FileDiagnosticFileDiagnostic]] | |
diagnosticsdiagnostics filefile ((RightRight ideasideas))== | |
[[((filefile,, ShowDiagShowDiag,, ideaToDiagnosticideaToDiagnostic ii))|| ii<-<- ideasideas,, ideaSeverityideaSeverity ii /=/= IgnoreIgnore]] | |
diagnosticsdiagnostics filefile ((LeftLeft parseErrparseErr))== | |
[[((filefile,, ShowDiagShowDiag,, parseErrorToDiagnosticparseErrorToDiagnostic parseErrparseErr))]] | |
ideaToDiagnosticideaToDiagnostic:::: IdeaIdea->-> DiagnosticDiagnostic | |
ideaToDiagnosticideaToDiagnostic ideaidea== | |
LSP.DiagnosticLSP.Diagnostic{{ | |
_range_range== srcSpanToRangesrcSpanToRange $$ ideaSpanideaSpan ideaidea | |
,, _severity_severity== JustJust LSP.DsInfoLSP.DsInfo | |
,, _code_code== JustJust ((LSP.StringValueLSP.StringValue $$ T.packT.pack $$ ideaHintideaHint ideaidea)) | |
,, _source_source== JustJust "hlint""hlint" | |
,, _message_message== T.packT.pack $$ showshow ideaidea | |
,, _relatedInformation_relatedInformation== NothingNothing | |
,, _tags_tags== NothingNothing | |
}} | |
parseErrorToDiagnosticparseErrorToDiagnostic:::: ParseErrorParseError->-> DiagnosticDiagnostic | |
parseErrorToDiagnosticparseErrorToDiagnostic ((Hlint.ParseErrorHlint.ParseError ll msgmsg contentscontents))== | |
LSP.DiagnosticLSP.Diagnostic{{ | |
_range_range== srcSpanToRangesrcSpanToRange ll | |
,, _severity_severity== JustJust LSP.DsInfoLSP.DsInfo | |
,, _code_code== JustJust ((LSP.StringValueLSP.StringValue "parser""parser")) | |
,, _source_source== JustJust "hlint""hlint" | |
,, _message_message== T.unlinesT.unlines [[T.packT.pack msgmsg,,T.packT.pack contentscontents]] | |
,, _relatedInformation_relatedInformation== NothingNothing | |
,, _tags_tags== NothingNothing | |
} | |
-- This one is defined in Development.IDE.GHC.Error but here | |
-- the types could come from ghc-lib or ghc | |
-- This one is defined in Development.IDE.GHC.Error but here | |
-- the types could come from ghc-lib or ghc | |
srcSpanToRangesrcSpanToRange:::: SrcSpanSrcSpan->-> LSP.RangeLSP.Range | |
srcSpanToRangesrcSpanToRange ((RealSrcSpanRealSrcSpan spanspan))== RangeRange{{ | |
_start_start== LSP.PositionLSP.Position{{ | |
_line_line== srcSpanStartLinesrcSpanStartLine spanspan -- 11 | |
,, _character_character== srcSpanStartColsrcSpanStartCol spanspan -- 11}} | |
,, _end_end== LSP.PositionLSP.Position{{ | |
_line_line== srcSpanEndLinesrcSpanEndLine spanspan -- 11 | |
,, _character_character== srcSpanEndColsrcSpanEndCol spanspan -- 11}} | |
}} | |
srcSpanToRangesrcSpanToRange ((UnhelpfulSpanUnhelpfulSpan __))== noRangenoRange | |
getIdeasgetIdeas:::: NormalizedFilePathNormalizedFilePath->-> ActionAction ((EitherEither ParseErrorParseError [[IdeaIdea]])) | |
getIdeasgetIdeas nfpnfp== dodo | |
logmlogm $$ "getIdeas:file:""getIdeas:file:" ++++ showshow nfpnfp | |
((flagsflags,, classifyclassify,, hinthint))<-<- useNoFile_useNoFile_ GetHlintSettingsGetHlintSettings | |
letlet applyHints'applyHints' ((JustJust ((RightRight modExmodEx))))== RightRight $$ applyHintsapplyHints classifyclassify hinthint [[modExmodEx]] | |
applyHints'applyHints' ((JustJust ((LeftLeft errerr))))== LeftLeft errerr | |
applyHints'applyHints' NothingNothing== RightRight [[]] | |
fmapfmap applyHints'applyHints' ((moduleExmoduleEx flagsflags)) | |
wherewhere moduleExmoduleEx:::: ParseFlagsParseFlags->-> ActionAction ((MaybeMaybe ((EitherEither ParseErrorParseError ModuleExModuleEx)))) | |
#ifndef GHC_LIB | |
moduleExmoduleEx _flags_flags== dodo | |
mbpmmbpm<-<- getParsedModulegetParsedModule nfpnfp | |
casecase mbpmmbpmofof | |
NothingNothing ->-> returnreturn NothingNothing | |
JustJust pmpm ->-> dodo | |
letlet annsanns== pm_annotationspm_annotations pmpm | |
letlet modumodu== pm_parsed_sourcepm_parsed_source pmpm | |
returnreturn $$ JustJust $$ RightRight ((createModuleExcreateModuleEx annsanns modumodu) | |
-- --------------------------------------------------------------------- | |
#else | |
moduleEx flags = do | |
flags' <- setExtensions flags | |
Just <$> (liftIO $ parseModuleEx flags' (fromNormalizedFilePath nfp) Nothing) | |
setExtensions flags = do | |
hsc <- hscEnv <$> use_ GhcSession nfp | |
let dflags = hsc_dflags hsc | |
let hscExts = EnumSet.toList (extensionFlags dflags) | |
logm $ "getIdeas:setExtensions:hscExtensions:" ++ show hscExts | |
let hlintExts = mapMaybe (GhclibParserEx.readExtension . show) hscExts | |
logm $ "getIdeas:setExtensions:hlintExtensions:" ++ show hlintExts | |
return $ flags { enabledExtensions = hlintExts } | |
#endif | |
-- --------------------------------------------------------------------- | |
datadata HlintUsageHlintUsage | |
== HlintEnabledHlintEnabled {{ cmdArgscmdArgs:::: [[StringString]]}} | |
|| HlintDisabledHlintDisabled | |
derivingderiving ShowShow | |
datadata GetHlintSettingsGetHlintSettings== GetHlintSettingsGetHlintSettings | |
derivingderiving ((EqEq,, ShowShow,, TypeableTypeable,, GenericGeneric)) | |
instanceinstance HashableHashable GetHlintSettingsGetHlintSettings | |
instanceinstance NFDataNFData GetHlintSettingsGetHlintSettings | |
instanceinstance NFDataNFData HintHintwherewhere rnfrnf== rwhnfrwhnf | |
instanceinstance NFDataNFData ClassifyClassifywherewhere rnfrnf== rwhnfrwhnf | |
instanceinstance NFDataNFData ParseFlagsParseFlagswherewhere rnfrnf== rwhnfrwhnf | |
instanceinstance ShowShow HintHintwherewhere showshow== constconst "<hint>""<hint>" | |
instanceinstance ShowShow ParseFlagsParseFlagswherewhere showshow== constconst "<parseFlags>""<parseFlags>" | |
instanceinstance BinaryBinary GetHlintSettingsGetHlintSettings | |
typetypeinstanceinstance RuleResultRuleResult GetHlintSettingsGetHlintSettings== ((ParseFlagsParseFlags,, [[ClassifyClassify]],, HintHint)) | |
getHlintSettingsRulegetHlintSettingsRule:::: HlintUsageHlintUsage->-> RulesRules (()) | |
getHlintSettingsRulegetHlintSettingsRule usageusage== | |
defineNoFiledefineNoFile $$ \\GetHlintSettingsGetHlintSettings->-> | |
liftIOliftIO $$ casecase usageusageofof | |
HlintEnabledHlintEnabled cmdArgscmdArgs ->-> argsSettingsargsSettings cmdArgscmdArgs | |
HlintDisabledHlintDisabled ->-> failfail "hlint configuration unspecified" | |
-- --------------------------------------------------------------------- | |
-- --------------------------------------------------------------------- | |
codeActionProvidercodeActionProvider:::: CodeActionProviderCodeActionProvider | |
codeActionProvidercodeActionProvider __ __ plIdplId docIddocId __ contextcontext== ((RightRight .. LSP.ListLSP.List .. mapmap CACodeActionCACodeAction)) <$><$> hlintActionshlintActions | |
wherewhere | |
hlintActionshlintActions:::: IOIO [[LSP.CodeActionLSP.CodeAction]] | |
hlintActionshlintActions== catMaybescatMaybes <$><$> mapMmapM mkHlintActionmkHlintAction ((filterfilter validCommandvalidCommand diagsdiags) | |
-- |Some hints do not have an associated refactoring | |
-- |Some hints do not have an associated refactoring | |
validCommandvalidCommand ((LSP.DiagnosticLSP.Diagnostic __ __ ((JustJust ((LSP.StringValueLSP.StringValue codecode)))) ((JustJust "hlint""hlint")) __ __ __))== | |
codecode /=/= "Eta reduce""Eta reduce" | |
validCommandvalidCommand __== FalseFalse | |
LSP.ListLSP.List diagsdiags== contextcontext ^.^. LSP.diagnosticsLSP.diagnostics | |
mkHlintActionmkHlintAction:::: LSP.DiagnosticLSP.Diagnostic->-> IOIO ((MaybeMaybe LSP.CodeActionLSP.CodeAction)) | |
mkHlintActionmkHlintAction diagdiag@@((LSP.DiagnosticLSP.Diagnostic ((LSP.RangeLSP.Range startstart __)) _s_s ((JustJust ((LSP.StringValueLSP.StringValue codecode)))) ((JustJust "hlint""hlint")) mm __ __))== | |
JustJust .. codeActioncodeAction <$><$> mkLspCommandmkLspCommand plIdplId "applyOne""applyOne" titletitle ((JustJust argsargs)) | |
wherewhere | |
codeActioncodeAction cmdcmd== LSP.CodeActionLSP.CodeAction titletitle ((JustJust LSP.CodeActionQuickFixLSP.CodeActionQuickFix)) ((JustJust ((LSP.ListLSP.List [[diagdiag]])))) NothingNothing ((JustJust cmdcmd)) | |
titletitle== "Apply hint:""Apply hint:" <><> headhead ((T.linesT.lines mm) | |
-- need 'file', 'start_pos' and hint title (to distinguish between alternative suggestions at the same location) | |
-- need 'file', 'start_pos' and hint title (to distinguish between alternative suggestions at the same location) | |
argsargs== [[toJSONtoJSON ((AOPAOP ((docIddocId ^.^. LSP.uriLSP.uri)) startstart codecode))]] | |
mkHlintActionmkHlintAction ((LSP.DiagnosticLSP.Diagnostic _r_r _s_s _c_c _source_source _m_m __ __))== returnreturn Nothing | |
-- --------------------------------------------------------------------- | |
-- --------------------------------------------------------------------- | |
applyAllCmdapplyAllCmd:::: CommandFunctionCommandFunction UriUri | |
applyAllCmdapplyAllCmd _lf_lf ideide uriuri== dodo | |
letlet filefile== maybemaybe ((errorerror $$ showshow uriuri ++++ " is not a file."" is not a file.")) | |
toNormalizedFilePath'toNormalizedFilePath' | |
((uriToFilePath'uriToFilePath' uriuri)) | |
logmlogm $$ "applyAllCmd:file=""applyAllCmd:file=" ++++ showshow filefile | |
resres<-<- applyHintapplyHint ideide filefile NothingNothing | |
logmlogm $$ "applyAllCmd:res=""applyAllCmd:res=" ++++ showshow resres | |
returnreturn $$ | |
casecase resresofof | |
LeftLeft errerr ->-> ((LeftLeft ((responseErrorresponseError ((T.packT.pack $$ "applyAll: ""applyAll: " ++++ showshow errerr)))),, NothingNothing)) | |
RightRight fsfs ->-> ((RightRight NullNull,, JustJust ((WorkspaceApplyEditWorkspaceApplyEdit,, ApplyWorkspaceEditParamsApplyWorkspaceEditParams fsfs))) | |
-- --------------------------------------------------------------------- | |
-- --------------------------------------------------------------------- | |
datadata ApplyOneParamsApplyOneParams== AOPAOP | |
{{ filefile :::: UriUri | |
,, start_posstart_pos:::: Position | |
-- | There can be more than one hint suggested at the same position, so HintTitle is used to distinguish between them. | |
-- | There can be more than one hint suggested at the same position, so HintTitle is used to distinguish between them. | |
,, hintTitlehintTitle:::: HintTitleHintTitle | |
}} derivingderiving ((EqEq,,ShowShow,,GenericGeneric,,FromJSONFromJSON,,ToJSONToJSON)) | |
typetype HintTitleHintTitle== T.TextT.Text | |
datadata OneHintOneHint== OneHintOneHint | |
{{ oneHintPosoneHintPos:::: PositionPosition | |
,, oneHintTitleoneHintTitle:::: HintTitleHintTitle | |
}} derivingderiving ((EqEq,, ShowShow)) | |
applyOneCmdapplyOneCmd:::: CommandFunctionCommandFunction ApplyOneParamsApplyOneParams | |
applyOneCmdapplyOneCmd _lf_lf ideide ((AOPAOP uriuri pospos titletitle))== dodo | |
letlet oneHintoneHint== OneHintOneHint pospos titletitle | |
letlet filefile== maybemaybe ((errorerror $$ showshow uriuri ++++ " is not a file."" is not a file.")) toNormalizedFilePath'toNormalizedFilePath' | |
((uriToFilePath'uriToFilePath' uriuri)) | |
resres<-<- applyHintapplyHint ideide filefile ((JustJust oneHintoneHint)) | |
logmlogm $$ "applyOneCmd:file=""applyOneCmd:file=" ++++ showshow filefile | |
logmlogm $$ "applyOneCmd:res=""applyOneCmd:res=" ++++ showshow resres | |
returnreturn $$ | |
casecase resresofof | |
LeftLeft errerr ->-> ((LeftLeft ((responseErrorresponseError ((T.packT.pack $$ "applyOne: ""applyOne: " ++++ showshow errerr)))),, NothingNothing)) | |
RightRight fsfs ->-> ((RightRight NullNull,, JustJust ((WorkspaceApplyEditWorkspaceApplyEdit,, ApplyWorkspaceEditParamsApplyWorkspaceEditParams fsfs)))) | |
applyHintapplyHint:::: IdeStateIdeState->-> NormalizedFilePathNormalizedFilePath->-> MaybeMaybe OneHintOneHint->-> IOIO ((EitherEither StringString WorkspaceEditWorkspaceEdit)) | |
applyHintapplyHint ideide nfpnfp mhintmhint== | |
runExceptTrunExceptT $$ dodo | |
ideasideas<-<- bimapExceptTbimapExceptT showParseErrorshowParseError idid $$ ExceptTExceptT $$ liftIOliftIO $$ runActionrunAction "applyHint""applyHint" ideide $$ getIdeasgetIdeas nfpnfp | |
letlet ideas'ideas'== maybemaybe ideasideas ((``filterIdeasfilterIdeas`` ideasideas)) mhintmhint | |
letlet commandscommands== mapmap ((showshow &&&&&& ideaRefactoringideaRefactoring)) ideas'ideas' | |
liftIOliftIO $$ logmlogm $$ "applyHint:apply=""applyHint:apply=" ++++ showshow commands | |
-- set Nothing as "position" for "applyRefactorings" because | |
-- applyRefactorings expects the provided position to be _within_ the scope | |
-- of each refactoring it will apply. | |
-- But "Idea"s returned by HLint point to starting position of the expressions | |
-- that contain refactorings, so they are often outside the refactorings' boundaries. | |
-- Example: | |
-- Given an expression "hlintTest = reid $ (myid ())" | |
-- Hlint returns an idea at the position (1,13) | |
-- That contains "Redundant brackets" refactoring at position (1,20): | |
-- | |
-- [("src/App/Test.hs:5:13: Warning: Redundant bracket\nFound:\n reid $ (myid ())\nWhy not:\n reid $ myid ()\n",[Replace {rtype = Expr, pos = SrcSpan {startLine = 5, startCol = 20, endLine = 5, endCol = 29}, subts = [("x",SrcSpan {startLine = 5, startCol = 21, endLine = 5, endCol = 28})], orig = "x"}])] | |
-- | |
-- If we provide "applyRefactorings" with "Just (1,13)" then | |
-- the "Redundant bracket" hint will never be executed | |
-- because SrcSpan (1,20,??,??) doesn't contain position (1,13). | |
-- set Nothing as "position" for "applyRefactorings" because | |
-- applyRefactorings expects the provided position to be _within_ the scope | |
-- of each refactoring it will apply. | |
-- But "Idea"s returned by HLint point to starting position of the expressions | |
-- that contain refactorings, so they are often outside the refactorings' boundaries. | |
-- Example: | |
-- Given an expression "hlintTest = reid $ (myid ())" | |
-- Hlint returns an idea at the position (1,13) | |
-- That contains "Redundant brackets" refactoring at position (1,20): | |
-- | |
-- [("src/App/Test.hs:5:13: Warning: Redundant bracket\nFound:\n reid $ (myid ())\nWhy not:\n reid $ myid ()\n",[Replace {rtype = Expr, pos = SrcSpan {startLine = 5, startCol = 20, endLine = 5, endCol = 29}, subts = [("x",SrcSpan {startLine = 5, startCol = 21, endLine = 5, endCol = 28})], orig = "x"}])] | |
-- | |
-- If we provide "applyRefactorings" with "Just (1,13)" then | |
-- the "Redundant bracket" hint will never be executed | |
-- because SrcSpan (1,20,??,??) doesn't contain position (1,13). | |
letlet fpfp== fromNormalizedFilePathfromNormalizedFilePath nfpnfp | |
resres<-<- liftIOliftIO $$ ((RightRight <$><$> applyRefactoringsapplyRefactorings NothingNothing commandscommands fpfp)) ``catchescatches`` | |
[[ HandlerHandler $$ \\ee->-> returnreturn ((LeftLeft ((showshow ((ee:::: IOExceptionIOException)))))) | |
,, HandlerHandler $$ \\ee->-> returnreturn ((LeftLeft ((showshow ((ee:::: ErrorCallErrorCall)))))) | |
]] | |
casecase resresofof | |
RightRight appliedFileappliedFile ->-> dodo | |
letlet uriuri== fromNormalizedUrifromNormalizedUri ((filePathToUri'filePathToUri' nfpnfp)) | |
oldContentoldContent<-<- liftIOliftIO $$ T.readFileT.readFile fpfp | |
liftIOliftIO $$ logmlogm $$ "applyHint:oldContent=""applyHint:oldContent=" ++++ showshow oldContentoldContent | |
liftIOliftIO $$ logmlogm $$ "applyHint:appliedFile=""applyHint:appliedFile=" ++++ showshow ((T.packT.pack appliedFileappliedFile)) | |
letlet wsEditwsEdit== diffText'diffText' TrueTrue ((uriuri,, oldContentoldContent)) ((T.packT.pack appliedFileappliedFile)) IncludeDeletionsIncludeDeletions | |
liftIOliftIO $$ logmlogm $$ "applyHint:diff=""applyHint:diff=" ++++ showshow wsEditwsEdit | |
ExceptTExceptT $$ RightRight <$><$> ((returnreturn wsEditwsEdit)) | |
LeftLeft errerr ->-> | |
throwEthrowE ((showshow errerr)) | |
where | |
-- | If we are only interested in applying a particular hint then | |
-- let's filter out all the irrelevant ideas | |
-- | If we are only interested in applying a particular hint then | |
-- let's filter out all the irrelevant ideas | |
filterIdeasfilterIdeas:::: OneHintOneHint->-> [[IdeaIdea]]->-> [[IdeaIdea]] | |
filterIdeasfilterIdeas ((OneHintOneHint ((PositionPosition ll cc)) titletitle)) ideasideas== | |
letlet title'title'== T.unpackT.unpack titletitle | |
ideaPosideaPos== ((srcSpanStartLinesrcSpanStartLine &&&&&& srcSpanStartColsrcSpanStartCol)) .. toRealSrcSpantoRealSrcSpan .. ideaSpanideaSpan | |
inin filterfilter ((\\ii->-> ideaHintideaHint ii ==== title'title' &&&& ideaPosideaPos ii ==== ((ll++11,, cc++11)))) ideasideas | |
toRealSrcSpantoRealSrcSpan ((RealSrcSpanRealSrcSpan realreal))== realreal | |
toRealSrcSpantoRealSrcSpan ((UnhelpfulSpanUnhelpfulSpan xx))== errorerror $$ "No real source span: ""No real source span: " ++++ showshow xx | |
showParseErrorshowParseError:::: Hlint.ParseErrorHlint.ParseError->-> StringString | |
showParseErrorshowParseError ((Hlint.ParseErrorHlint.ParseError locationlocation messagemessage contentcontent))== | |
unlinesunlines [[showshow locationlocation,, messagemessage,, contentcontent] | |
-- | Map over both failure and success. | |
-- | Map over both failure and success. | |
bimapExceptTbimapExceptT:::: FunctorFunctor mm=>=> ((ee->-> ff))->-> ((aa->-> bb))->-> ExceptTExceptT ee mm aa->-> ExceptTExceptT ff mm bb | |
bimapExceptTbimapExceptT ff gg ((ExceptTExceptT mm))== ExceptTExceptT ((fmapfmap hh mm))wherewhere | |
hh ((LeftLeft ee)) == LeftLeft ((ff ee)) | |
hh ((RightRight aa))== RightRight ((gg aa)) | |
{-# INLINE{-# INLINE bimapExceptTbimapExceptT#-} | |
-- --------------------------------------------------------------------- | |
{- | |
{-# LANGUAGE CPP #-} | |
{-# LANGUAGE DeriveAnyClass #-} | |
{-# LANGUAGE DeriveGeneric #-} | |
{-# LANGUAGE DuplicateRecordFields #-} | |
{-# LANGUAGE OverloadedStrings #-} | |
-- | apply-refact applies refactorings specified by the refact package. It is | |
-- currently integrated into hlint to enable the automatic application of | |
-- suggestions. | |
module Haskell.Ide.Engine.Plugin.ApplyRefact where | |
import Control.Arrow | |
import Control.Exception ( IOException | |
, ErrorCall | |
, Handler(..) | |
, catches | |
, try | |
) | |
import Control.Lens hiding ( List ) | |
import Control.Monad.IO.Class | |
import Control.Monad.Trans.Except | |
import Data.Aeson hiding (Error) | |
import Data.Maybe | |
import qualified Data.Text as T | |
import GHC.Generics | |
import Haskell.Ide.Engine.MonadFunctions | |
import Haskell.Ide.Engine.MonadTypes | |
import Haskell.Ide.Engine.PluginUtils | |
import Language.Haskell.Exts.SrcLoc | |
import Language.Haskell.Exts.Parser | |
import Language.Haskell.Exts.Extension | |
import Language.Haskell.HLint4 as Hlint | |
import qualified Language.Haskell.LSP.Types as LSP | |
import qualified Language.Haskell.LSP.Types.Lens as LSP | |
import Refact.Apply | |
-- --------------------------------------------------------------------- | |
{-# ANN module ("HLint: ignore Eta reduce" :: String) #-} | |
{-# ANN module ("HLint: ignore Redundant do" :: String) #-} | |
{-# ANN module ("HLint: ignore Reduce duplication" :: String) #-} | |
-- --------------------------------------------------------------------- | |
type HintTitle = T.Text | |
applyRefactDescriptor :: PluginId -> PluginDescriptor | |
applyRefactDescriptor plId = PluginDescriptor | |
{ pluginId = plId | |
, pluginName = "ApplyRefact" | |
, pluginDesc = "apply-refact applies refactorings specified by the refact package. It is currently integrated into hlint to enable the automatic application of suggestions." | |
, pluginCommands = | |
[ PluginCommand "applyOne" "Apply a single hint" applyOneCmd | |
, PluginCommand "applyAll" "Apply all hints to the file" applyAllCmd | |
] | |
, pluginCodeActionProvider = Just codeActionProvider | |
, pluginDiagnosticProvider = Nothing | |
, pluginHoverProvider = Nothing | |
, pluginSymbolProvider = Nothing | |
, pluginFormattingProvider = Nothing | |
} | |
-- --------------------------------------------------------------------- | |
data ApplyOneParams = AOP | |
{ file :: Uri | |
, start_pos :: Position | |
-- | There can be more than one hint suggested at the same position, so HintTitle is used to distinguish between them. | |
, hintTitle :: HintTitle | |
} deriving (Eq,Show,Generic,FromJSON,ToJSON) | |
data OneHint = OneHint | |
{ oneHintPos :: Position | |
, oneHintTitle :: HintTitle | |
} deriving (Eq, Show) | |
applyOneCmd :: ApplyOneParams -> IdeGhcM (IdeResult WorkspaceEdit) | |
applyOneCmd (AOP uri pos title) = pluginGetFile "applyOne: " uri $ \fp -> do | |
let oneHint = OneHint pos title | |
revMapp <- reverseFileMap | |
let defaultResult = do | |
debugm "applyOne: no access to the persisted file." | |
return $ IdeResultOk mempty | |
withMappedFile fp defaultResult $ \file' -> do | |
res <- liftToGhc $ applyHint file' (Just oneHint) revMapp | |
logm $ "applyOneCmd:file=" ++ show fp | |
logm $ "applyOneCmd:res=" ++ show res | |
case res of | |
Left err -> return $ IdeResultFail | |
(IdeError PluginError (T.pack $ "applyOne: " ++ show err) Null) | |
Right fs -> return (IdeResultOk fs) | |
-- --------------------------------------------------------------------- | |
applyAllCmd :: Uri -> IdeGhcM (IdeResult WorkspaceEdit) | |
applyAllCmd uri = pluginGetFile "applyAll: " uri $ \fp -> do | |
let defaultResult = do | |
debugm "applyAll: no access to the persisted file." | |
return $ IdeResultOk mempty | |
revMapp <- reverseFileMap | |
withMappedFile fp defaultResult $ \file' -> do | |
res <- liftToGhc $ applyHint file' Nothing revMapp | |
logm $ "applyAllCmd:res=" ++ show res | |
case res of | |
Left err -> return $ IdeResultFail (IdeError PluginError | |
(T.pack $ "applyAll: " ++ show err) Null) | |
Right fs -> return (IdeResultOk fs) | |
-- --------------------------------------------------------------------- | |
-- AZ:TODO: Why is this in IdeGhcM? | |
lint :: Uri -> IdeGhcM (IdeResult PublishDiagnosticsParams) | |
lint uri = pluginGetFile "lint: " uri $ \fp -> do | |
let | |
defaultResult = do | |
debugm "lint: no access to the persisted file." | |
return | |
$ IdeResultOk (PublishDiagnosticsParams (filePathToUri fp) $ List []) | |
withMappedFile fp defaultResult $ \file' -> do | |
eitherErrorResult <- liftIO | |
(try $ runExceptT $ runLint file' [] :: IO | |
(Either IOException (Either [Diagnostic] [Idea])) | |
) | |
case eitherErrorResult of | |
Left err -> return $ IdeResultFail | |
(IdeError PluginError (T.pack $ "lint: " ++ show err) Null) | |
Right res -> case res of | |
Left diags -> | |
return | |
(IdeResultOk | |
(PublishDiagnosticsParams (filePathToUri fp) $ List diags) | |
) | |
Right fs -> | |
return | |
$ IdeResultOk | |
$ PublishDiagnosticsParams (filePathToUri fp) | |
$ List (map hintToDiagnostic $ stripIgnores fs) | |
runLint :: FilePath -> [String] -> ExceptT [Diagnostic] IO [Idea] | |
runLint fp args = do | |
(flags,classify,hint) <- liftIO $ argsSettings args | |
let myflags = flags { hseFlags = (hseFlags flags) { extensions = EnableExtension TypeApplications:extensions (hseFlags flags)}} | |
res <- bimapExceptT parseErrorToDiagnostic id $ ExceptT $ parseModuleEx myflags fp Nothing | |
pure $ applyHints classify hint [res] | |
parseErrorToDiagnostic :: Hlint.ParseError -> [Diagnostic] | |
parseErrorToDiagnostic (Hlint.ParseError l msg contents) = | |
[Diagnostic | |
{ _range = srcLoc2Range l | |
, _severity = Just DsInfo -- Not displayed | |
, _code = Just (LSP.StringValue "parser") | |
, _source = Just "hlint" | |
, _message = T.unlines [T.pack msg,T.pack contents] | |
, _relatedInformation = Nothing | |
}] | |
{- | |
-- | An idea suggest by a 'Hint'. | |
data Idea = Idea | |
{ideaModule :: String -- ^ The module the idea applies to, may be @\"\"@ if the module cannot be determined or is a result of cross-module hints. | |
,ideaDecl :: String -- ^ The declaration the idea applies to, typically the function name, but may be a type name. | |
,ideaSeverity :: Severity -- ^ The severity of the idea, e.g. 'Warning'. | |
,ideaHint :: String -- ^ The name of the hint that generated the idea, e.g. @\"Use reverse\"@. | |
,ideaSpan :: SrcSpan -- ^ The source code the idea relates to. | |
,ideaFrom :: String -- ^ The contents of the source code the idea relates to. | |
,ideaTo :: Maybe String -- ^ The suggested replacement, or 'Nothing' for no replacement (e.g. on parse errors). | |
,ideaNote :: [Note] -- ^ Notes about the effect of applying the replacement. | |
,ideaRefactoring :: [Refactoring R.SrcSpan] -- ^ How to perform this idea | |
} | |
deriving (Eq,Ord) | |
-} | |
-- | Map over both failure and success. | |
bimapExceptT :: Functor m => (e -> f) -> (a -> b) -> ExceptT e m a -> ExceptT f m b | |
bimapExceptT f g (ExceptT m) = ExceptT (fmap h m) where | |
h (Left e) = Left (f e) | |
h (Right a) = Right (g a) | |
{-# INLINE bimapExceptT #-} | |
-- --------------------------------------------------------------------- | |
stripIgnores :: [Idea] -> [Idea] | |
stripIgnores ideas = filter notIgnored ideas | |
where | |
notIgnored idea = ideaSeverity idea /= Ignore | |
-- --------------------------------------------------------------------- | |
hintToDiagnostic :: Idea -> Diagnostic | |
hintToDiagnostic idea | |
= Diagnostic | |
{ _range = ss2Range (ideaSpan idea) | |
, _severity = Just (hintSeverityMap $ ideaSeverity idea) | |
, _code = Just (LSP.StringValue $ T.pack $ ideaHint idea) | |
, _source = Just "hlint" | |
, _message = idea2Message idea | |
, _relatedInformation = Nothing | |
} | |
-- --------------------------------------------------------------------- | |
idea2Message :: Idea -> T.Text | |
idea2Message idea = T.unlines $ [T.pack $ ideaHint idea, "Found:", " " <> T.pack (ideaFrom idea)] | |
<> toIdea <> map (T.pack . show) (ideaNote idea) | |
where | |
toIdea :: [T.Text] | |
toIdea = case ideaTo idea of | |
Nothing -> [] | |
Just i -> [T.pack "Why not:", T.pack $ " " ++ i] | |
-- --------------------------------------------------------------------- | |
-- | Maps hlint severities to LSP severities | |
-- | We want to lower the severities so HLint errors and warnings | |
-- | don't mix with GHC errors and warnings: | |
-- | as per https://github.com/haskell/haskell-ide-engine/issues/375 | |
hintSeverityMap :: Severity -> DiagnosticSeverity | |
hintSeverityMap Ignore = DsInfo -- cannot really happen after stripIgnores | |
hintSeverityMap Suggestion = DsHint | |
hintSeverityMap Warning = DsInfo | |
hintSeverityMap Error = DsInfo | |
-- --------------------------------------------------------------------- | |
srcLoc2Range :: SrcLoc -> Range | |
srcLoc2Range (SrcLoc _ l c) = Range ps pe | |
where | |
ps = Position (l-1) (c-1) | |
pe = Position (l-1) 100000 | |
-- --------------------------------------------------------------------- | |
ss2Range :: SrcSpan -> Range | |
ss2Range ss = Range ps pe | |
where | |
ps = Position (srcSpanStartLine ss - 1) (srcSpanStartColumn ss - 1) | |
pe = Position (srcSpanEndLine ss - 1) (srcSpanEndColumn ss - 1) | |
-- --------------------------------------------------------------------- | |
applyHint :: FilePath -> Maybe OneHint -> (FilePath -> FilePath) -> IdeM (Either String WorkspaceEdit) | |
applyHint fp mhint fileMap = do | |
runExceptT $ do | |
ideas <- getIdeas fp mhint | |
let commands = map (show &&& ideaRefactoring) ideas | |
liftIO $ logm $ "applyHint:apply=" ++ show commands | |
-- set Nothing as "position" for "applyRefactorings" because | |
-- applyRefactorings expects the provided position to be _within_ the scope | |
-- of each refactoring it will apply. | |
-- But "Idea"s returned by HLint pont to starting position of the expressions | |
-- that contain refactorings, so they are often outside the refactorings' boundaries. | |
-- Example: | |
-- Given an expression "hlintTest = reid $ (myid ())" | |
-- Hlint returns an idea at the position (1,13) | |
-- That contains "Redundant brackets" refactoring at position (1,20): | |
-- | |
-- [("src/App/Test.hs:5:13: Warning: Redundant bracket\nFound:\n reid $ (myid ())\nWhy not:\n reid $ myid ()\n",[Replace {rtype = Expr, pos = SrcSpan {startLine = 5, startCol = 20, endLine = 5, endCol = 29}, subts = [("x",SrcSpan {startLine = 5, startCol = 21, endLine = 5, endCol = 28})], orig = "x"}])] | |
-- | |
-- If we provide "applyRefactorings" with "Just (1,13)" then | |
-- the "Redundant bracket" hint will never be executed | |
-- because SrcSpan (1,20,??,??) doesn't contain position (1,13). | |
res <- liftIO $ (Right <$> applyRefactorings Nothing commands fp) `catches` | |
[ Handler $ \e -> return (Left (show (e :: IOException))) | |
, Handler $ \e -> return (Left (show (e :: ErrorCall))) | |
] | |
case res of | |
Right appliedFile -> do | |
diff <- ExceptT $ Right <$> makeDiffResult fp (T.pack appliedFile) fileMap | |
liftIO $ logm $ "applyHint:diff=" ++ show diff | |
return diff | |
Left err -> | |
throwE (show err) | |
-- | Gets HLint ideas for | |
getIdeas :: MonadIO m => FilePath -> Maybe OneHint -> ExceptT String m [Idea] | |
getIdeas lintFile mhint = do | |
let hOpts = hlintOpts lintFile (oneHintPos <$> mhint) | |
ideas <- runHlint lintFile hOpts | |
pure $ maybe ideas (`filterIdeas` ideas) mhint | |
-- | If we are only interested in applying a particular hint then | |
-- let's filter out all the irrelevant ideas | |
filterIdeas :: OneHint -> [Idea] -> [Idea] | |
filterIdeas (OneHint (Position l c) title) ideas = | |
let | |
title' = T.unpack title | |
ideaPos = (srcSpanStartLine &&& srcSpanStartColumn) . ideaSpan | |
in filter (\i -> ideaHint i == title' && ideaPos i == (l+1, c+1)) ideas | |
hlintOpts :: FilePath -> Maybe Position -> [String] | |
hlintOpts lintFile mpos = | |
let | |
posOpt (Position l c) = " --pos " ++ show (l+1) ++ "," ++ show (c+1) | |
opts = maybe "" posOpt mpos | |
in [lintFile, "--quiet", "--refactor", "--refactor-options=" ++ opts ] | |
runHlint :: MonadIO m => FilePath -> [String] -> ExceptT String m [Idea] | |
runHlint fp args = | |
do (flags,classify,hint) <- liftIO $ argsSettings args | |
let myflags = flags { hseFlags = (hseFlags flags) { extensions = EnableExtension TypeApplications:extensions (hseFlags flags)}} | |
res <- bimapExceptT showParseError id $ ExceptT $ liftIO $ parseModuleEx myflags fp Nothing | |
pure $ applyHints classify hint [res] | |
showParseError :: Hlint.ParseError -> String | |
showParseError (Hlint.ParseError location message content) = | |
unlines [show location, message, content] | |
-- --------------------------------------------------------------------- | |
codeActionProvider :: CodeActionProvider | |
codeActionProvider plId docId _ context = IdeResultOk <$> hlintActions | |
where | |
hlintActions :: IdeM [LSP.CodeAction] | |
hlintActions = catMaybes <$> mapM mkHlintAction (filter validCommand diags) | |
-- |Some hints do not have an associated refactoring | |
validCommand (LSP.Diagnostic _ _ (Just (LSP.StringValue code)) (Just "hlint") _ _) = | |
case code of | |
"Eta reduce" -> False | |
_ -> True | |
validCommand _ = False | |
LSP.List diags = context ^. LSP.diagnostics | |
mkHlintAction :: LSP.Diagnostic -> IdeM (Maybe LSP.CodeAction) | |
mkHlintAction diag@(LSP.Diagnostic (LSP.Range start _) _s (Just (LSP.StringValue code)) (Just "hlint") m _) = | |
Just . codeAction <$> mkLspCommand plId "applyOne" title (Just args) | |
where | |
codeAction cmd = LSP.CodeAction title (Just LSP.CodeActionQuickFix) (Just (LSP.List [diag])) Nothing (Just cmd) | |
title = "Apply hint:" <> head (T.lines m) | |
-- need 'file', 'start_pos' and hint title (to distinguish between alternative suggestions at the same location) | |
args = [toJSON (AOP (docId ^. LSP.uri) start code)] | |
mkHlintAction (LSP.Diagnostic _r _s _c _source _m _) = return Nothing | |
-} | |
-- --------------------------------------------------------------------- | |
{- | |
{-# LANGUAGE CPP #-} | |
{-# LANGUAGE DeriveAnyClass #-} | |
{-# LANGUAGE DeriveGeneric #-} | |
{-# LANGUAGE DuplicateRecordFields #-} | |
{-# LANGUAGE OverloadedStrings #-} | |
-- | apply-refact applies refactorings specified by the refact package. It is | |
-- currently integrated into hlint to enable the automatic application of | |
-- suggestions. | |
module Haskell.Ide.Engine.Plugin.ApplyRefact where | |
import Control.Arrow | |
import Control.Exception ( IOException | |
, ErrorCall | |
, Handler(..) | |
, catches | |
, try | |
) | |
import Control.Lens hiding ( List ) | |
import Control.Monad.IO.Class | |
import Control.Monad.Trans.Except | |
import Data.Aeson hiding (Error) | |
import Data.Maybe | |
import Data.Monoid ((<>)) | |
import qualified Data.Text as T | |
import GHC.Generics | |
import Haskell.Ide.Engine.MonadFunctions | |
import Haskell.Ide.Engine.MonadTypes | |
import Haskell.Ide.Engine.PluginUtils | |
import Language.Haskell.Exts.SrcLoc | |
import Language.Haskell.Exts.Parser | |
import Language.Haskell.Exts.Extension | |
import Language.Haskell.HLint4 as Hlint | |
import qualified Language.Haskell.LSP.Types as LSP | |
import qualified Language.Haskell.LSP.Types.Lens as LSP | |
import Refact.Apply | |
-- --------------------------------------------------------------------- | |
{-# ANN module ("HLint: ignore Eta reduce" :: String) #-} | |
{-# ANN module ("HLint: ignore Redundant do" :: String) #-} | |
{-# ANN module ("HLint: ignore Reduce duplication" :: String) #-} | |
-- --------------------------------------------------------------------- | |
type HintTitle = T.Text | |
applyRefactDescriptor :: PluginId -> PluginDescriptor | |
applyRefactDescriptor plId = PluginDescriptor | |
{ pluginId = plId | |
, pluginName = "ApplyRefact" | |
, pluginDesc = "apply-refact applies refactorings specified by the refact package. It is currently integrated into hlint to enable the automatic application of suggestions." | |
, pluginCommands = | |
[ PluginCommand "applyOne" "Apply a single hint" applyOneCmd | |
, PluginCommand "applyAll" "Apply all hints to the file" applyAllCmd | |
] | |
, pluginCodeActionProvider = Just codeActionProvider | |
, pluginDiagnosticProvider = Nothing | |
, pluginHoverProvider = Nothing | |
, pluginSymbolProvider = Nothing | |
, pluginFormattingProvider = Nothing | |
} | |
-- --------------------------------------------------------------------- | |
data ApplyOneParams = AOP | |
{ file :: Uri | |
, start_pos :: Position | |
-- | There can be more than one hint suggested at the same position, so HintTitle is used to distinguish between them. | |
, hintTitle :: HintTitle | |
} deriving (Eq,Show,Generic,FromJSON,ToJSON) | |
data OneHint = OneHint | |
{ oneHintPos :: Position | |
, oneHintTitle :: HintTitle | |
} deriving (Eq, Show) | |
applyOneCmd :: ApplyOneParams -> IdeGhcM (IdeResult WorkspaceEdit) | |
applyOneCmd (AOP uri pos title) = pluginGetFile "applyOne: " uri $ \fp -> do | |
let oneHint = OneHint pos title | |
revMapp <- reverseFileMap | |
let defaultResult = do | |
debugm "applyOne: no access to the persisted file." | |
return $ IdeResultOk mempty | |
withMappedFile fp defaultResult $ \file' -> do | |
res <- liftToGhc $ applyHint file' (Just oneHint) revMapp | |
logm $ "applyOneCmd:file=" ++ show fp | |
logm $ "applyOneCmd:res=" ++ show res | |
case res of | |
Left err -> return $ IdeResultFail | |
(IdeError PluginError (T.pack $ "applyOne: " ++ show err) Null) | |
Right fs -> return (IdeResultOk fs) | |
-- --------------------------------------------------------------------- | |
applyAllCmd :: Uri -> IdeGhcM (IdeResult WorkspaceEdit) | |
applyAllCmd uri = pluginGetFile "applyAll: " uri $ \fp -> do | |
let defaultResult = do | |
debugm "applyAll: no access to the persisted file." | |
return $ IdeResultOk mempty | |
revMapp <- reverseFileMap | |
withMappedFile fp defaultResult $ \file' -> do | |
res <- liftToGhc $ applyHint file' Nothing revMapp | |
logm $ "applyAllCmd:res=" ++ show res | |
case res of | |
Left err -> return $ IdeResultFail (IdeError PluginError | |
(T.pack $ "applyAll: " ++ show err) Null) | |
Right fs -> return (IdeResultOk fs) | |
-- --------------------------------------------------------------------- | |
-- AZ:TODO: Why is this in IdeGhcM? | |
lint :: Uri -> IdeGhcM (IdeResult PublishDiagnosticsParams) | |
lint uri = pluginGetFile "lint: " uri $ \fp -> do | |
let | |
defaultResult = do | |
debugm "lint: no access to the persisted file." | |
return | |
$ IdeResultOk (PublishDiagnosticsParams (filePathToUri fp) $ List []) | |
withMappedFile fp defaultResult $ \file' -> do | |
eitherErrorResult <- liftIO | |
(try $ runExceptT $ runLint file' [] :: IO | |
(Either IOException (Either [Diagnostic] [Idea])) | |
) | |
case eitherErrorResult of | |
Left err -> return $ IdeResultFail | |
(IdeError PluginError (T.pack $ "lint: " ++ show err) Null) | |
Right res -> case res of | |
Left diags -> | |
return | |
(IdeResultOk | |
(PublishDiagnosticsParams (filePathToUri fp) $ List diags) | |
) | |
Right fs -> | |
return | |
$ IdeResultOk | |
$ PublishDiagnosticsParams (filePathToUri fp) | |
$ List (map hintToDiagnostic $ stripIgnores fs) | |
runLint :: FilePath -> [String] -> ExceptT [Diagnostic] IO [Idea] | |
runLint fp args = do | |
(flags,classify,hint) <- liftIO $ argsSettings args | |
let myflags = flags { hseFlags = (hseFlags flags) { extensions = EnableExtension TypeApplications:extensions (hseFlags flags)}} | |
res <- bimapExceptT parseErrorToDiagnostic id $ ExceptT $ parseModuleEx myflags fp Nothing | |
pure $ applyHints classify hint [res] | |
parseErrorToDiagnostic :: Hlint.ParseError -> [Diagnostic] | |
parseErrorToDiagnostic (Hlint.ParseError l msg contents) = | |
[Diagnostic | |
{ _range = srcLoc2Range l | |
, _severity = Just DsInfo -- Not displayed | |
, _code = Just (LSP.StringValue "parser") | |
, _source = Just "hlint" | |
, _message = T.unlines [T.pack msg,T.pack contents] | |
, _relatedInformation = Nothing | |
}] | |
{- | |
-- | An idea suggest by a 'Hint'. | |
data Idea = Idea | |
{ideaModule :: String -- ^ The module the idea applies to, may be @\"\"@ if the module cannot be determined or is a result of cross-module hints. | |
,ideaDecl :: String -- ^ The declaration the idea applies to, typically the function name, but may be a type name. | |
,ideaSeverity :: Severity -- ^ The severity of the idea, e.g. 'Warning'. | |
,ideaHint :: String -- ^ The name of the hint that generated the idea, e.g. @\"Use reverse\"@. | |
,ideaSpan :: SrcSpan -- ^ The source code the idea relates to. | |
,ideaFrom :: String -- ^ The contents of the source code the idea relates to. | |
,ideaTo :: Maybe String -- ^ The suggested replacement, or 'Nothing' for no replacement (e.g. on parse errors). | |
,ideaNote :: [Note] -- ^ Notes about the effect of applying the replacement. | |
,ideaRefactoring :: [Refactoring R.SrcSpan] -- ^ How to perform this idea | |
} | |
deriving (Eq,Ord) | |
-} | |
-- | Map over both failure and success. | |
bimapExceptT :: Functor m => (e -> f) -> (a -> b) -> ExceptT e m a -> ExceptT f m b | |
bimapExceptT f g (ExceptT m) = ExceptT (fmap h m) where | |
h (Left e) = Left (f e) | |
h (Right a) = Right (g a) | |
{-# INLINE bimapExceptT #-} | |
-- --------------------------------------------------------------------- | |
stripIgnores :: [Idea] -> [Idea] | |
stripIgnores ideas = filter notIgnored ideas | |
where | |
notIgnored idea = ideaSeverity idea /= Ignore | |
-- --------------------------------------------------------------------- | |
hintToDiagnostic :: Idea -> Diagnostic | |
hintToDiagnostic idea | |
= Diagnostic | |
{ _range = ss2Range (ideaSpan idea) | |
, _severity = Just (hintSeverityMap $ ideaSeverity idea) | |
, _code = Just (LSP.StringValue $ T.pack $ ideaHint idea) | |
, _source = Just "hlint" | |
, _message = idea2Message idea | |
, _relatedInformation = Nothing | |
} | |
-- --------------------------------------------------------------------- | |
idea2Message :: Idea -> T.Text | |
idea2Message idea = T.unlines $ [T.pack $ ideaHint idea, "Found:", " " <> T.pack (ideaFrom idea)] | |
<> toIdea <> map (T.pack . show) (ideaNote idea) | |
where | |
toIdea :: [T.Text] | |
toIdea = case ideaTo idea of | |
Nothing -> [] | |
Just i -> [T.pack "Why not:", T.pack $ " " ++ i] | |
-- --------------------------------------------------------------------- | |
-- | Maps hlint severities to LSP severities | |
-- | We want to lower the severities so HLint errors and warnings | |
-- | don't mix with GHC errors and warnings: | |
-- | as per https://github.com/haskell/haskell-ide-engine/issues/375 | |
hintSeverityMap :: Severity -> DiagnosticSeverity | |
hintSeverityMap Ignore = DsInfo -- cannot really happen after stripIgnores | |
hintSeverityMap Suggestion = DsHint | |
hintSeverityMap Warning = DsInfo | |
hintSeverityMap Error = DsInfo | |
-- --------------------------------------------------------------------- | |
srcLoc2Range :: SrcLoc -> Range | |
srcLoc2Range (SrcLoc _ l c) = Range ps pe | |
where | |
ps = Position (l-1) (c-1) | |
pe = Position (l-1) 100000 | |
-- --------------------------------------------------------------------- | |
ss2Range :: SrcSpan -> Range | |
ss2Range ss = Range ps pe | |
where | |
ps = Position (srcSpanStartLine ss - 1) (srcSpanStartColumn ss - 1) | |
pe = Position (srcSpanEndLine ss - 1) (srcSpanEndColumn ss - 1) | |
-- --------------------------------------------------------------------- | |
applyHint :: FilePath -> Maybe OneHint -> (FilePath -> FilePath) -> IdeM (Either String WorkspaceEdit) | |
applyHint fp mhint fileMap = do | |
runExceptT $ do | |
ideas <- getIdeas fp mhint | |
let commands = map (show &&& ideaRefactoring) ideas | |
liftIO $ logm $ "applyHint:apply=" ++ show commands | |
-- set Nothing as "position" for "applyRefactorings" because | |
-- applyRefactorings expects the provided position to be _within_ the scope | |
-- of each refactoring it will apply. | |
-- But "Idea"s returned by HLint pont to starting position of the expressions | |
-- that contain refactorings, so they are often outside the refactorings' boundaries. | |
-- Example: | |
-- Given an expression "hlintTest = reid $ (myid ())" | |
-- Hlint returns an idea at the position (1,13) | |
-- That contains "Redundant brackets" refactoring at position (1,20): | |
-- | |
-- [("src/App/Test.hs:5:13: Warning: Redundant bracket\nFound:\n reid $ (myid ())\nWhy not:\n reid $ myid ()\n",[Replace {rtype = Expr, pos = SrcSpan {startLine = 5, startCol = 20, endLine = 5, endCol = 29}, subts = [("x",SrcSpan {startLine = 5, startCol = 21, endLine = 5, endCol = 28})], orig = "x"}])] | |
-- | |
-- If we provide "applyRefactorings" with "Just (1,13)" then | |
-- the "Redundant bracket" hint will never be executed | |
-- because SrcSpan (1,20,??,??) doesn't contain position (1,13). | |
res <- liftIO $ (Right <$> applyRefactorings Nothing commands fp) `catches` | |
[ Handler $ \e -> return (Left (show (e :: IOException))) | |
, Handler $ \e -> return (Left (show (e :: ErrorCall))) | |
] | |
case res of | |
Right appliedFile -> do | |
diff <- ExceptT $ Right <$> makeDiffResult fp (T.pack appliedFile) fileMap | |
liftIO $ logm $ "applyHint:diff=" ++ show diff | |
return diff | |
Left err -> | |
throwE (show err) | |
-- | Gets HLint ideas for | |
getIdeas :: MonadIO m => FilePath -> Maybe OneHint -> ExceptT String m [Idea] | |
getIdeas lintFile mhint = do | |
let hOpts = hlintOpts lintFile (oneHintPos <$> mhint) | |
ideas <- runHlint lintFile hOpts | |
pure $ maybe ideas (`filterIdeas` ideas) mhint | |
-- | If we are only interested in applying a particular hint then | |
-- let's filter out all the irrelevant ideas | |
filterIdeas :: OneHint -> [Idea] -> [Idea] | |
filterIdeas (OneHint (Position l c) title) ideas = | |
let | |
title' = T.unpack title | |
ideaPos = (srcSpanStartLine &&& srcSpanStartColumn) . ideaSpan | |
in filter (\i -> ideaHint i == title' && ideaPos i == (l+1, c+1)) ideas | |
hlintOpts :: FilePath -> Maybe Position -> [String] | |
hlintOpts lintFile mpos = | |
let | |
posOpt (Position l c) = " --pos " ++ show (l+1) ++ "," ++ show (c+1) | |
opts = maybe "" posOpt mpos | |
in [lintFile, "--quiet", "--refactor", "--refactor-options=" ++ opts ] | |
runHlint :: MonadIO m => FilePath -> [String] -> ExceptT String m [Idea] | |
runHlint fp args = | |
do (flags,classify,hint) <- liftIO $ argsSettings args | |
let myflags = flags { hseFlags = (hseFlags flags) { extensions = EnableExtension TypeApplications:extensions (hseFlags flags)}} | |
res <- bimapExceptT showParseError id $ ExceptT $ liftIO $ parseModuleEx myflags fp Nothing | |
pure $ applyHints classify hint [res] | |
showParseError :: Hlint.ParseError -> String | |
showParseError (Hlint.ParseError location message content) = | |
unlines [show location, message, content] | |
-- --------------------------------------------------------------------- | |
codeActionProvider :: CodeActionProvider | |
codeActionProvider plId docId _ context = IdeResultOk <$> hlintActions | |
where | |
hlintActions :: IdeM [LSP.CodeAction] | |
hlintActions = catMaybes <$> mapM mkHlintAction (filter validCommand diags) | |
-- |Some hints do not have an associated refactoring | |
validCommand (LSP.Diagnostic _ _ (Just (LSP.StringValue code)) (Just "hlint") _ _) = | |
case code of | |
"Eta reduce" -> False | |
_ -> True | |
validCommand _ = False | |
LSP.List diags = context ^. LSP.diagnostics | |
mkHlintAction :: LSP.Diagnostic -> IdeM (Maybe LSP.CodeAction) | |
mkHlintAction diag@(LSP.Diagnostic (LSP.Range start _) _s (Just (LSP.StringValue code)) (Just "hlint") m _) = | |
Just . codeAction <$> mkLspCommand plId "applyOne" title (Just args) | |
where | |
codeAction cmd = LSP.CodeAction title (Just LSP.CodeActionQuickFix) (Just (LSP.List [diag])) Nothing (Just cmd) | |
title = "Apply hint:" <> head (T.lines m) | |
-- need 'file', 'start_pos' and hint title (to distinguish between alternative suggestions at the same location) | |
args = [toJSON (AOP (docId ^. LSP.uri) start code)] | |
mkHlintAction (LSP.Diagnostic _r _s _c _source _m _) = return Nothing | |
-} | |
#endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment