From c90fd07ffbbc1deb15f4a426bcd3a4bf0c7dee7a Mon Sep 17 00:00:00 2001 From: Potato Hatsue <1793913507@qq.com> Date: Sun, 21 Feb 2021 13:17:21 +0800 Subject: [PATCH 1/9] Refactor pragmas plugin --- .../src/Development/IDE/Plugin/CodeAction.hs | 23 -- ghcide/test/exe/Main.hs | 112 ++------ plugins/default/src/Ide/Plugin/Pragmas.hs | 268 ++++++++++-------- test/functional/Class.hs | 1 - test/functional/FunctionalCodeAction.hs | 61 +++- 5 files changed, 236 insertions(+), 229 deletions(-) diff --git a/ghcide/src/Development/IDE/Plugin/CodeAction.hs b/ghcide/src/Development/IDE/Plugin/CodeAction.hs index 3ee947af84..8d93479b66 100644 --- a/ghcide/src/Development/IDE/Plugin/CodeAction.hs +++ b/ghcide/src/Development/IDE/Plugin/CodeAction.hs @@ -158,7 +158,6 @@ suggestAction packageExports ideOptions parsedModule text df annSource tcM har d ++ suggestNewImport packageExports pm diag ++ suggestDeleteUnusedBinding pm text diag ++ suggestExportUnusedTopBinding text pm diag - ++ suggestDisableWarning pm text diag | Just pm <- [parsedModule] ] ++ suggestFillHole diag -- Lowest priority @@ -257,15 +256,6 @@ isUnusedImportedId maybe True (not . any (\(_, IdentifierDetails {..}) -> identInfo == S.singleton Use)) refs | otherwise = False -suggestDisableWarning :: ParsedModule -> Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])] -suggestDisableWarning pm contents Diagnostic{..} - | Just (InR (T.stripPrefix "-W" -> Just w)) <- _code = - pure - ( "Disable \"" <> w <> "\" warnings" - , [TextEdit (endOfModuleHeader pm contents) $ "{-# OPTIONS_GHC -Wno-" <> w <> " #-}\n"] - ) - | otherwise = [] - suggestRemoveRedundantImport :: ParsedModule -> Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])] suggestRemoveRedundantImport ParsedModule{pm_parsed_source = L _ HsModule{hsmodImports}} contents Diagnostic{_range=_range,..} -- The qualified import of ‘many’ from module ‘Control.Applicative’ is redundant @@ -1452,16 +1442,3 @@ renderImportStyle :: ImportStyle -> T.Text renderImportStyle (ImportTopLevel x) = x renderImportStyle (ImportViaParent x p) = p <> "(" <> x <> ")" --- | Find the first non-blank line before the first of (module name / imports / declarations). --- Useful for inserting pragmas. -endOfModuleHeader :: ParsedModule -> Maybe T.Text -> Range -endOfModuleHeader pm contents = - let mod = unLoc $ pm_parsed_source pm - modNameLoc = getLoc <$> hsmodName mod - firstImportLoc = getLoc <$> listToMaybe (hsmodImports mod) - firstDeclLoc = getLoc <$> listToMaybe (hsmodDecls mod) - line = fromMaybe 0 $ firstNonBlankBefore . _line . _start =<< srcSpanToRange =<< - modNameLoc <|> firstImportLoc <|> firstDeclLoc - firstNonBlankBefore n = (n -) . fromMaybe 0 . findIndex (not . T.null) . reverse . take n . T.lines <$> contents - loc = Position line 0 - in Range loc loc diff --git a/ghcide/test/exe/Main.hs b/ghcide/test/exe/Main.hs index 4c15476bf8..0cfb5c7e56 100644 --- a/ghcide/test/exe/Main.hs +++ b/ghcide/test/exe/Main.hs @@ -81,7 +81,6 @@ import Development.IDE.Plugin.Test (TestRequest (BlockSeconds, GetInterfaceFiles import Control.Monad.Extra (whenJust) import qualified Language.LSP.Types.Lens as L import Control.Lens ((^.)) -import Data.Functor import Data.Tuple.Extra waitForProgressBegin :: Session () @@ -706,7 +705,6 @@ codeActionTests = testGroup "code actions" , suggestImportTests , suggestHideShadowTests , suggestImportDisambiguationTests - , disableWarningTests , fixConstructorImportTests , importRenameActionTests , fillTypedHoleTests @@ -913,8 +911,9 @@ removeImportTests = testGroup "remove import actions" ] docB <- createDoc "ModuleB.hs" "haskell" contentB _ <- waitForDiagnostics - action <- assertJust "Code action not found" . firstJust (caWithTitle "Remove import") - =<< getCodeActions docB (Range (Position 2 0) (Position 2 5)) + [InR action@CodeAction { _title = actionTitle }, _] + <- getCodeActions docB (Range (Position 2 0) (Position 2 5)) + liftIO $ "Remove import" @=? actionTitle executeCodeAction action contentAfterAction <- documentContents docB let expectedContentAfterAction = T.unlines @@ -938,8 +937,9 @@ removeImportTests = testGroup "remove import actions" ] docB <- createDoc "ModuleB.hs" "haskell" contentB _ <- waitForDiagnostics - action <- assertJust "Code action not found" . firstJust (caWithTitle "Remove import") - =<< getCodeActions docB (Range (Position 2 0) (Position 2 5)) + [InR action@CodeAction { _title = actionTitle }, _] + <- getCodeActions docB (Range (Position 2 0) (Position 2 5)) + liftIO $ "Remove import" @=? actionTitle executeCodeAction action contentAfterAction <- documentContents docB let expectedContentAfterAction = T.unlines @@ -966,8 +966,9 @@ removeImportTests = testGroup "remove import actions" ] docB <- createDoc "ModuleB.hs" "haskell" contentB _ <- waitForDiagnostics - action <- assertJust "Code action not found" . firstJust (caWithTitle "Remove stuffA, stuffC from import") - =<< getCodeActions docB (Range (Position 2 0) (Position 2 5)) + [InR action@CodeAction { _title = actionTitle }, _] + <- getCodeActions docB (Range (Position 2 0) (Position 2 5)) + liftIO $ "Remove stuffA, stuffC from import" @=? actionTitle executeCodeAction action contentAfterAction <- documentContents docB let expectedContentAfterAction = T.unlines @@ -994,8 +995,9 @@ removeImportTests = testGroup "remove import actions" ] docB <- createDoc "ModuleB.hs" "haskell" contentB _ <- waitForDiagnostics - action <- assertJust "Code action not found" . firstJust (caWithTitle "Remove !!, from import") - =<< getCodeActions docB (Range (Position 2 0) (Position 2 5)) + [InR action@CodeAction { _title = actionTitle }, _] + <- getCodeActions docB (Range (Position 2 0) (Position 2 5)) + liftIO $ "Remove !!, from import" @=? actionTitle executeCodeAction action contentAfterAction <- documentContents docB let expectedContentAfterAction = T.unlines @@ -1021,8 +1023,9 @@ removeImportTests = testGroup "remove import actions" ] docB <- createDoc "ModuleB.hs" "haskell" contentB _ <- waitForDiagnostics - action <- assertJust "Code action not found" . firstJust (caWithTitle "Remove A from import") - =<< getCodeActions docB (Range (Position 2 0) (Position 2 5)) + [InR action@CodeAction { _title = actionTitle }, _] + <- getCodeActions docB (Range (Position 2 0) (Position 2 5)) + liftIO $ "Remove A from import" @=? actionTitle executeCodeAction action contentAfterAction <- documentContents docB let expectedContentAfterAction = T.unlines @@ -1047,8 +1050,9 @@ removeImportTests = testGroup "remove import actions" ] docB <- createDoc "ModuleB.hs" "haskell" contentB _ <- waitForDiagnostics - action <- assertJust "Code action not found" . firstJust (caWithTitle "Remove A, E, F from import") - =<< getCodeActions docB (Range (Position 2 0) (Position 2 5)) + [InR action@CodeAction { _title = actionTitle }, _] + <- getCodeActions docB (Range (Position 2 0) (Position 2 5)) + liftIO $ "Remove A, E, F from import" @=? actionTitle executeCodeAction action contentAfterAction <- documentContents docB let expectedContentAfterAction = T.unlines @@ -1070,8 +1074,9 @@ removeImportTests = testGroup "remove import actions" ] docB <- createDoc "ModuleB.hs" "haskell" contentB _ <- waitForDiagnostics - action <- assertJust "Code action not found" . firstJust (caWithTitle "Remove import") - =<< getCodeActions docB (Range (Position 2 0) (Position 2 5)) + [InR action@CodeAction { _title = actionTitle }, _] + <- getCodeActions docB (Range (Position 2 0) (Position 2 5)) + liftIO $ "Remove import" @=? actionTitle executeCodeAction action contentAfterAction <- documentContents docB let expectedContentAfterAction = T.unlines @@ -1094,8 +1099,9 @@ removeImportTests = testGroup "remove import actions" ] doc <- createDoc "ModuleC.hs" "haskell" content _ <- waitForDiagnostics - action <- assertJust "Code action not found" . firstJust (caWithTitle "Remove all redundant imports") - =<< getCodeActions doc (Range (Position 2 0) (Position 2 5)) + [_, _, _, _, InR action@CodeAction { _title = actionTitle }] + <- getCodeActions doc (Range (Position 2 0) (Position 2 5)) + liftIO $ "Remove all redundant imports" @=? actionTitle executeCodeAction action contentAfterAction <- documentContents doc let expectedContentAfterAction = T.unlines @@ -1111,10 +1117,6 @@ removeImportTests = testGroup "remove import actions" ] liftIO $ expectedContentAfterAction @=? contentAfterAction ] - where - caWithTitle t = \case - InR a@CodeAction{_title} -> guard (_title == t) >> Just a - _ -> Nothing extendImportTests :: TestTree extendImportTests = testGroup "extend import actions" @@ -1784,57 +1786,6 @@ suggestHideShadowTests = , "(++) = id" ] -disableWarningTests :: TestTree -disableWarningTests = - testGroup "disable warnings" $ - [ - ( "missing-signatures" - , T.unlines - [ "{-# OPTIONS_GHC -Wall #-}" - , "main = putStrLn \"hello\"" - ] - , T.unlines - [ "{-# OPTIONS_GHC -Wall #-}" - , "{-# OPTIONS_GHC -Wno-missing-signatures #-}" - , "main = putStrLn \"hello\"" - ] - ) - , - ( "unused-imports" - , T.unlines - [ "{-# OPTIONS_GHC -Wall #-}" - , "" - , "" - , "module M where" - , "" - , "import Data.Functor" - ] - , T.unlines - [ "{-# OPTIONS_GHC -Wall #-}" - , "{-# OPTIONS_GHC -Wno-unused-imports #-}" - , "" - , "" - , "module M where" - , "" - , "import Data.Functor" - ] - ) - ] - <&> \(warning, initialContent, expectedContent) -> testSession (T.unpack warning) $ do - doc <- createDoc "Module.hs" "haskell" initialContent - _ <- waitForDiagnostics - codeActs <- mapMaybe caResultToCodeAct <$> getCodeActions doc (Range (Position 0 0) (Position 0 0)) - case find (\CodeAction{_title} -> _title == "Disable \"" <> warning <> "\" warnings") codeActs of - Nothing -> liftIO $ assertFailure "No code action with expected title" - Just action -> do - executeCodeAction action - contentAfterAction <- documentContents doc - liftIO $ expectedContent @=? contentAfterAction - where - caResultToCodeAct = \case - InL _ -> Nothing - InR c -> Just c - insertNewDefinitionTests :: TestTree insertNewDefinitionTests = testGroup "insert new definition actions" [ testSession "insert new function definition" $ do @@ -2586,12 +2537,7 @@ removeRedundantConstraintsTests = let doc <- createDoc "Testing.hs" "haskell" code _ <- waitForDiagnostics actionsOrCommands <- getCodeActions doc (Range (Position 4 0) (Position 4 maxBound)) - liftIO $ assertBool "Found some actions (other than \"disable warnings\")" - $ all isDisableWarningAction actionsOrCommands - where - isDisableWarningAction = \case - InR CodeAction{_title} -> "Disable" `T.isPrefixOf` _title && "warnings" `T.isSuffixOf` _title - _ -> False + liftIO $ assertBool "Found some actions" (null actionsOrCommands) in testGroup "remove redundant function constraints" [ check @@ -4786,9 +4732,7 @@ asyncTests = testGroup "async" void waitForDiagnostics actions <- getCodeActions doc (Range (Position 1 0) (Position 1 0)) liftIO $ [ _title | InR CodeAction{_title} <- actions] @=? - [ "add signature: foo :: a -> a" - , "Disable \"missing-signatures\" warnings" - ] + [ "add signature: foo :: a -> a" ] , testSession "request" $ do -- Execute a custom request that will block for 1000 seconds void $ sendRequest (SCustomMethod "test") $ toJSON $ BlockSeconds 1000 @@ -4800,9 +4744,7 @@ asyncTests = testGroup "async" void waitForDiagnostics actions <- getCodeActions doc (Range (Position 0 0) (Position 0 0)) liftIO $ [ _title | InR CodeAction{_title} <- actions] @=? - [ "add signature: foo :: a -> a" - , "Disable \"missing-signatures\" warnings" - ] + [ "add signature: foo :: a -> a" ] ] diff --git a/plugins/default/src/Ide/Plugin/Pragmas.hs b/plugins/default/src/Ide/Plugin/Pragmas.hs index bef200645a..00dc3cbf9e 100644 --- a/plugins/default/src/Ide/Plugin/Pragmas.hs +++ b/plugins/default/src/Ide/Plugin/Pragmas.hs @@ -1,89 +1,102 @@ {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ViewPatterns #-} -- | Provides code actions to add missing pragmas (whenever GHC suggests to) -module Ide.Plugin.Pragmas - ( - descriptor - ) where - -import Control.Lens hiding (List) -import qualified Data.HashMap.Strict as H -import Data.Maybe (catMaybes) -import qualified Data.Text as T -import Development.IDE as D +module Ide.Plugin.Pragmas (descriptor) where + +import Control.Applicative ((<|>)) +import Control.Lens hiding (List) +import Control.Monad (join) +import Control.Monad.IO.Class +import qualified Data.HashMap.Strict as H +import Data.List.Extra (findIndex, nubOrdOn) +import Data.Maybe (catMaybes, fromMaybe, listToMaybe) +import qualified Data.Text as T +import Development.IDE as D +import Development.IDE.GHC.Compat import Ide.Types +import qualified Language.LSP.Server as LSP import Language.LSP.Types -import qualified Language.LSP.Types as J -import qualified Language.LSP.Types.Lens as J +import qualified Language.LSP.Types as J +import qualified Language.LSP.Types.Lens as J +import qualified Language.LSP.VFS as VFS +import qualified Text.Fuzzy as Fuzzy -import Control.Monad (join) -import Development.IDE.GHC.Compat -import qualified Language.LSP.Server as LSP -import qualified Language.LSP.VFS as VFS -import qualified Text.Fuzzy as Fuzzy -import Data.List.Extra (nubOrd) -import Control.Monad.IO.Class +descriptor :: PluginId -> PluginDescriptor IdeState +descriptor plId = + (defaultPluginDescriptor plId) + { pluginHandlers = + mkPluginHandler STextDocumentCodeAction codeActionProvider + <> mkPluginHandler STextDocumentCompletion completion + } -- --------------------------------------------------------------------- -descriptor :: PluginId -> PluginDescriptor IdeState -descriptor plId = (defaultPluginDescriptor plId) - { pluginHandlers = mkPluginHandler STextDocumentCodeAction codeActionProvider - <> mkPluginHandler STextDocumentCompletion completion - } +-- | Title and pragma +type PragmaEdit = (T.Text, Pragma) --- --------------------------------------------------------------------- +data Pragma = LangExt T.Text | OptGHC T.Text + deriving (Show, Eq, Ord) + +codeActionProvider :: PluginMethodHandler IdeState TextDocumentCodeAction +codeActionProvider state _plId (CodeActionParams _ _ docId _ (J.CodeActionContext (J.List diags) _monly)) = do + let mFile = docId ^. J.uri & uriToFilePath <&> toNormalizedFilePath' + uri = docId ^. J.uri + content <- fmap VFS.virtualFileText <$> LSP.getVirtualFile (toNormalizedUri uri) + pm <- liftIO $ fmap join $ runAction "Pragmas.GetParsedModule" state $ getParsedModule `traverse` mFile + let dflags = ms_hspp_opts . pm_mod_summary <$> pm + insertRange = maybe (Range (Position 0 0) (Position 0 0)) (`endOfModuleHeader` content) pm + pedits = nubOrdOn snd . concat $ suggest dflags <$> diags + return $ Right $ List $ pragmaEditToAction uri insertRange <$> pedits -- | Add a Pragma to the given URI at the top of the file. --- Pragma is added to the first line of the Uri. -- It is assumed that the pragma name is a valid pragma, -- thus, not validated. -mkPragmaEdit :: Uri -> T.Text -> WorkspaceEdit -mkPragmaEdit uri pragmaName = res where - pos = J.Position 0 0 - textEdits = J.List - [J.TextEdit (J.Range pos pos) - ("{-# LANGUAGE " <> pragmaName <> " #-}\n") - ] - res = J.WorkspaceEdit - (Just $ H.singleton uri textEdits) - Nothing +pragmaEditToAction :: Uri -> Range -> PragmaEdit -> (Command |? CodeAction) +pragmaEditToAction uri range (title, p) = + InR $ J.CodeAction title (Just J.CodeActionQuickFix) (Just (J.List [])) Nothing Nothing (Just edit) Nothing + where + render (OptGHC x) = "{-# OPTIONS_GHC -Wno-" <> x <> " #-}\n" + render (LangExt x) = "{-# LANGUAGE " <> x <> " #-}\n" + textEdits = J.List [J.TextEdit range $ render p] + edit = + J.WorkspaceEdit + (Just $ H.singleton uri textEdits) + Nothing + +suggest :: Maybe DynFlags -> Diagnostic -> [PragmaEdit] +suggest dflags diag = + suggestAddPragma dflags diag + ++ suggestDisableWarning diag + +-- --------------------------------------------------------------------- + +suggestDisableWarning :: Diagnostic -> [PragmaEdit] +suggestDisableWarning Diagnostic {_code} + | Just (InR (T.stripPrefix "-W" -> Just w)) <- _code = + pure ("Disable \"" <> w <> "\" warnings", OptGHC w) + | otherwise = [] -- --------------------------------------------------------------------- + -- | Offer to add a missing Language Pragma to the top of a file. -- Pragmas are defined by a curated list of known pragmas, see 'possiblePragmas'. -codeActionProvider :: PluginMethodHandler IdeState TextDocumentCodeAction -codeActionProvider state _plId (CodeActionParams _ _ docId _ (J.CodeActionContext (J.List diags) _monly)) = liftIO $ do - let mFile = docId ^. J.uri & uriToFilePath <&> toNormalizedFilePath' - pm <- fmap join $ runAction "addPragma" state $ getParsedModule `traverse` mFile - let dflags = ms_hspp_opts . pm_mod_summary <$> pm - -- Get all potential Pragmas for all diagnostics. - pragmas = nubOrd $ concatMap (\d -> genPragma dflags (d ^. J.message)) diags - cmds <- mapM mkCodeAction pragmas - return $ Right $ List cmds +suggestAddPragma :: Maybe DynFlags -> Diagnostic -> [PragmaEdit] +suggestAddPragma mDynflags Diagnostic {_message} = genPragma _message + where + genPragma target = + [("Add \"" <> r <> "\"", LangExt r) | r <- findPragma target, r `notElem` disabled] where - mkCodeAction pragmaName = do - let - codeAction = InR $ J.CodeAction title (Just J.CodeActionQuickFix) (Just (J.List [])) Nothing Nothing (Just edit) Nothing - title = "Add \"" <> pragmaName <> "\"" - edit = mkPragmaEdit (docId ^. J.uri) pragmaName - return codeAction - - genPragma mDynflags target = - [ r | r <- findPragma target, r `notElem` disabled] - where - disabled - | Just dynFlags <- mDynflags - -- GHC does not export 'OnOff', so we have to view it as string - = catMaybes $ T.stripPrefix "Off " . T.pack . prettyPrint <$> extensions dynFlags - | otherwise - -- When the module failed to parse, we don't have access to its - -- dynFlags. In that case, simply don't disable any pragmas. - = [] - --- --------------------------------------------------------------------- + disabled + | Just dynFlags <- mDynflags = + -- GHC does not export 'OnOff', so we have to view it as string + catMaybes $ T.stripPrefix "Off " . T.pack . prettyPrint <$> extensions dynFlags + | otherwise = + -- When the module failed to parse, we don't have access to its + -- dynFlags. In that case, simply don't disable any pragmas. + [] -- | Find all Pragmas are an infix of the search term. findPragma :: T.Text -> [T.Text] @@ -98,69 +111,86 @@ findPragma str = concatMap check possiblePragmas -- extension in an error message. possiblePragmas :: [T.Text] possiblePragmas = - [ name - | FlagSpec{flagSpecName = T.pack -> name} <- xFlags - , "Strict" /= name - ] - --- --------------------------------------------------------------------- + [ name + | FlagSpec {flagSpecName = T.pack -> name} <- xFlags, + "Strict" /= name + ] -- | All language pragmas, including the No- variants allPragmas :: [T.Text] allPragmas = concat [ [name, "No" <> name] - | FlagSpec{flagSpecName = T.pack -> name} <- xFlags + | FlagSpec {flagSpecName = T.pack -> name} <- xFlags + ] + <> + -- These pragmas are not part of xFlags as they are not reversable + -- by prepending "No". + [ -- Safe Haskell + "Unsafe", + "Trustworthy", + "Safe", + -- Language Version Extensions + "Haskell98", + "Haskell2010" + -- Maybe, GHC 2021 after its release? ] - <> - -- These pragmas are not part of xFlags as they are not reversable - -- by prepending "No". - [ -- Safe Haskell - "Unsafe" - , "Trustworthy" - , "Safe" - - -- Language Version Extensions - , "Haskell98" - , "Haskell2010" - -- Maybe, GHC 2021 after its release? - ] -- --------------------------------------------------------------------- completion :: PluginMethodHandler IdeState TextDocumentCompletion completion _ide _ complParams = do - let (TextDocumentIdentifier uri) = complParams ^. J.textDocument - position = complParams ^. J.position - contents <- LSP.getVirtualFile $ toNormalizedUri uri - fmap (Right . InL) $ case (contents, uriToFilePath' uri) of - (Just cnts, Just _path) -> - result <$> VFS.getCompletionPrefix position cnts - where - result (Just pfix) - | "{-# LANGUAGE" `T.isPrefixOf` VFS.fullLine pfix - = List $ map buildCompletion - (Fuzzy.simpleFilter (VFS.prefixText pfix) allPragmas) - | otherwise - = List [] - result Nothing = List [] - buildCompletion p = - CompletionItem - { _label = p, - _kind = Just CiKeyword, - _tags = Nothing, - _detail = Nothing, - _documentation = Nothing, - _deprecated = Nothing, - _preselect = Nothing, - _sortText = Nothing, - _filterText = Nothing, - _insertText = Nothing, - _insertTextFormat = Nothing, - _textEdit = Nothing, - _additionalTextEdits = Nothing, - _commitCharacters = Nothing, - _command = Nothing, - _xdata = Nothing - } - _ -> return $ List [] + let (TextDocumentIdentifier uri) = complParams ^. J.textDocument + position = complParams ^. J.position + contents <- LSP.getVirtualFile $ toNormalizedUri uri + fmap (Right . InL) $ case (contents, uriToFilePath' uri) of + (Just cnts, Just _path) -> + result <$> VFS.getCompletionPrefix position cnts + where + result (Just pfix) + | "{-# LANGUAGE" `T.isPrefixOf` VFS.fullLine pfix = + List $ + map + buildCompletion + (Fuzzy.simpleFilter (VFS.prefixText pfix) allPragmas) + | otherwise = + List [] + result Nothing = List [] + buildCompletion p = + CompletionItem + { _label = p, + _kind = Just CiKeyword, + _tags = Nothing, + _detail = Nothing, + _documentation = Nothing, + _deprecated = Nothing, + _preselect = Nothing, + _sortText = Nothing, + _filterText = Nothing, + _insertText = Nothing, + _insertTextFormat = Nothing, + _textEdit = Nothing, + _additionalTextEdits = Nothing, + _commitCharacters = Nothing, + _command = Nothing, + _xdata = Nothing + } + _ -> return $ List [] + +-- --------------------------------------------------------------------- + +-- | Find the first non-blank line before the first of (module name / imports / declarations). +-- Useful for inserting pragmas. +endOfModuleHeader :: ParsedModule -> Maybe T.Text -> Range +endOfModuleHeader pm contents = + let mod = unLoc $ pm_parsed_source pm + modNameLoc = getLoc <$> hsmodName mod + firstImportLoc = getLoc <$> listToMaybe (hsmodImports mod) + firstDeclLoc = getLoc <$> listToMaybe (hsmodDecls mod) + line = + fromMaybe 0 $ + firstNonBlankBefore . _line . _start =<< srcSpanToRange + =<< modNameLoc <|> firstImportLoc <|> firstDeclLoc + firstNonBlankBefore n = (n -) . fromMaybe 0 . findIndex (not . T.null) . reverse . take n . T.lines <$> contents + loc = Position line 0 + in Range loc loc diff --git a/test/functional/Class.hs b/test/functional/Class.hs index 0a0a2d0d4d..d8c925cdb6 100644 --- a/test/functional/Class.hs +++ b/test/functional/Class.hs @@ -34,7 +34,6 @@ tests = testGroup @?= [ Just "Add placeholders for '=='" , Just "Add placeholders for '/='" - , Just "Disable \"missing-methods\" warnings" ] , glodenTest "Creates a placeholder for '=='" "T1" "eq" $ \(eqAction:_) -> do diff --git a/test/functional/FunctionalCodeAction.hs b/test/functional/FunctionalCodeAction.hs index a150c274c6..573a6423a7 100644 --- a/test/functional/FunctionalCodeAction.hs +++ b/test/functional/FunctionalCodeAction.hs @@ -1,6 +1,8 @@ +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE NamedFieldPuns #-} module FunctionalCodeAction (tests) where @@ -26,6 +28,7 @@ import Test.Tasty import Test.Tasty.ExpectedFailure (ignoreTestBecause, expectFailBecause) import Test.Tasty.HUnit import System.FilePath (()) +import System.IO.Extra (withTempDir) {-# ANN module ("HLint: ignore Reduce duplication"::String) #-} @@ -34,6 +37,7 @@ tests = testGroup "code actions" [ hlintTests , importTests , missingPragmaTests + , disableWarningTests , packageTests , redundantImportTests , renameTests @@ -376,7 +380,7 @@ redundantImportTests = testGroup "redundant import code actions" [ , testCase "doesn't touch other imports" $ runSession hlsCommand noLiteralCaps "test/testdata/redundantImportTest/" $ do doc <- openDoc "src/MultipleImports.hs" "haskell" _ <- waitForDiagnosticsFromSource doc "typecheck" - _ : InL cmd : _ <- getAllCodeActions doc + InL cmd : _ <- getAllCodeActions doc executeCommand cmd _ <- anyRequest contents <- documentContents doc @@ -554,6 +558,57 @@ missingPragmaTests = testGroup "missing pragma warning code actions" [ liftIO $ T.lines contents @?= expected ] +disableWarningTests :: TestTree +disableWarningTests = + testGroup "disable warnings" $ + [ + ( "missing-signatures" + , T.unlines + [ "{-# OPTIONS_GHC -Wall #-}" + , "main = putStrLn \"hello\"" + ] + , T.unlines + [ "{-# OPTIONS_GHC -Wall #-}" + , "{-# OPTIONS_GHC -Wno-missing-signatures #-}" + , "main = putStrLn \"hello\"" + ] + ) + , + ( "unused-imports" + , T.unlines + [ "{-# OPTIONS_GHC -Wall #-}" + , "" + , "" + , "module M where" + , "" + , "import Data.Functor" + ] + , T.unlines + [ "{-# OPTIONS_GHC -Wall #-}" + , "{-# OPTIONS_GHC -Wno-unused-imports #-}" + , "" + , "" + , "module M where" + , "" + , "import Data.Functor" + ] + ) + ] + <&> \(warning, initialContent, expectedContent) -> testSession (T.unpack warning) $ do + doc <- createDoc "Module.hs" "haskell" initialContent + _ <- waitForDiagnostics + codeActs <- mapMaybe caResultToCodeAct <$> getCodeActions doc (Range (Position 0 0) (Position 0 0)) + case find (\CodeAction{_title} -> _title == "Disable \"" <> warning <> "\" warnings") codeActs of + Nothing -> liftIO $ assertFailure "No code action with expected title" + Just action -> do + executeCodeAction action + contentAfterAction <- documentContents doc + liftIO $ expectedContent @=? contentAfterAction + where + caResultToCodeAct = \case + InL _ -> Nothing + InR c -> Just c + unusedTermTests :: TestTree unusedTermTests = testGroup "unused term code actions" [ ignoreTestBecause "no support for prefixing unused names with _" $ testCase "Prefixes with '_'" $ @@ -610,3 +665,7 @@ noLiteralCaps = def { C._textDocument = Just textDocumentCaps } where textDocumentCaps = def { C._codeAction = Just codeActionCaps } codeActionCaps = CodeActionClientCapabilities (Just True) Nothing Nothing + +testSession :: String -> Session () -> TestTree +testSession name s = testCase name $ withTempDir $ \dir -> + runSession hlsCommand fullCaps dir s From 7c771c6d69d9e2ec00949bdb110672916ef4df11 Mon Sep 17 00:00:00 2001 From: Potato Hatsue <1793913507@qq.com> Date: Sun, 21 Feb 2021 13:24:07 +0800 Subject: [PATCH 2/9] Revert some format changes --- plugins/default/src/Ide/Plugin/Pragmas.hs | 126 +++++++++++----------- 1 file changed, 62 insertions(+), 64 deletions(-) diff --git a/plugins/default/src/Ide/Plugin/Pragmas.hs b/plugins/default/src/Ide/Plugin/Pragmas.hs index 00dc3cbf9e..60219081e6 100644 --- a/plugins/default/src/Ide/Plugin/Pragmas.hs +++ b/plugins/default/src/Ide/Plugin/Pragmas.hs @@ -24,13 +24,13 @@ import qualified Language.LSP.Types.Lens as J import qualified Language.LSP.VFS as VFS import qualified Text.Fuzzy as Fuzzy +-- --------------------------------------------------------------------- + descriptor :: PluginId -> PluginDescriptor IdeState -descriptor plId = - (defaultPluginDescriptor plId) - { pluginHandlers = - mkPluginHandler STextDocumentCodeAction codeActionProvider - <> mkPluginHandler STextDocumentCompletion completion - } +descriptor plId = (defaultPluginDescriptor plId) + { pluginHandlers = mkPluginHandler STextDocumentCodeAction codeActionProvider + <> mkPluginHandler STextDocumentCompletion completion + } -- --------------------------------------------------------------------- @@ -88,15 +88,14 @@ suggestAddPragma mDynflags Diagnostic {_message} = genPragma _message where genPragma target = [("Add \"" <> r <> "\"", LangExt r) | r <- findPragma target, r `notElem` disabled] - where - disabled - | Just dynFlags <- mDynflags = - -- GHC does not export 'OnOff', so we have to view it as string - catMaybes $ T.stripPrefix "Off " . T.pack . prettyPrint <$> extensions dynFlags - | otherwise = - -- When the module failed to parse, we don't have access to its - -- dynFlags. In that case, simply don't disable any pragmas. - [] + disabled + | Just dynFlags <- mDynflags = + -- GHC does not export 'OnOff', so we have to view it as string + catMaybes $ T.stripPrefix "Off " . T.pack . prettyPrint <$> extensions dynFlags + | otherwise = + -- When the module failed to parse, we don't have access to its + -- dynFlags. In that case, simply don't disable any pragmas. + [] -- | Find all Pragmas are an infix of the search term. findPragma :: T.Text -> [T.Text] @@ -121,61 +120,60 @@ allPragmas :: [T.Text] allPragmas = concat [ [name, "No" <> name] - | FlagSpec {flagSpecName = T.pack -> name} <- xFlags - ] - <> - -- These pragmas are not part of xFlags as they are not reversable - -- by prepending "No". - [ -- Safe Haskell - "Unsafe", - "Trustworthy", - "Safe", - -- Language Version Extensions - "Haskell98", - "Haskell2010" - -- Maybe, GHC 2021 after its release? + | FlagSpec{flagSpecName = T.pack -> name} <- xFlags ] + <> + -- These pragmas are not part of xFlags as they are not reversable + -- by prepending "No". + [ -- Safe Haskell + "Unsafe" + , "Trustworthy" + , "Safe" + + -- Language Version Extensions + , "Haskell98" + , "Haskell2010" + -- Maybe, GHC 2021 after its release? + ] -- --------------------------------------------------------------------- completion :: PluginMethodHandler IdeState TextDocumentCompletion completion _ide _ complParams = do - let (TextDocumentIdentifier uri) = complParams ^. J.textDocument - position = complParams ^. J.position - contents <- LSP.getVirtualFile $ toNormalizedUri uri - fmap (Right . InL) $ case (contents, uriToFilePath' uri) of - (Just cnts, Just _path) -> - result <$> VFS.getCompletionPrefix position cnts - where - result (Just pfix) - | "{-# LANGUAGE" `T.isPrefixOf` VFS.fullLine pfix = - List $ - map - buildCompletion - (Fuzzy.simpleFilter (VFS.prefixText pfix) allPragmas) - | otherwise = - List [] - result Nothing = List [] - buildCompletion p = - CompletionItem - { _label = p, - _kind = Just CiKeyword, - _tags = Nothing, - _detail = Nothing, - _documentation = Nothing, - _deprecated = Nothing, - _preselect = Nothing, - _sortText = Nothing, - _filterText = Nothing, - _insertText = Nothing, - _insertTextFormat = Nothing, - _textEdit = Nothing, - _additionalTextEdits = Nothing, - _commitCharacters = Nothing, - _command = Nothing, - _xdata = Nothing - } - _ -> return $ List [] + let (TextDocumentIdentifier uri) = complParams ^. J.textDocument + position = complParams ^. J.position + contents <- LSP.getVirtualFile $ toNormalizedUri uri + fmap (Right . InL) $ case (contents, uriToFilePath' uri) of + (Just cnts, Just _path) -> + result <$> VFS.getCompletionPrefix position cnts + where + result (Just pfix) + | "{-# LANGUAGE" `T.isPrefixOf` VFS.fullLine pfix + = List $ map buildCompletion + (Fuzzy.simpleFilter (VFS.prefixText pfix) allPragmas) + | otherwise + = List [] + result Nothing = List [] + buildCompletion p = + CompletionItem + { _label = p, + _kind = Just CiKeyword, + _tags = Nothing, + _detail = Nothing, + _documentation = Nothing, + _deprecated = Nothing, + _preselect = Nothing, + _sortText = Nothing, + _filterText = Nothing, + _insertText = Nothing, + _insertTextFormat = Nothing, + _textEdit = Nothing, + _additionalTextEdits = Nothing, + _commitCharacters = Nothing, + _command = Nothing, + _xdata = Nothing + } + _ -> return $ List [] -- --------------------------------------------------------------------- From 8c0ea29a39f9124a860c2c1c15eace798566ecbc Mon Sep 17 00:00:00 2001 From: Potato Hatsue <1793913507@qq.com> Date: Sun, 21 Feb 2021 13:28:20 +0800 Subject: [PATCH 3/9] Revert some format changes --- plugins/default/src/Ide/Plugin/Pragmas.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/default/src/Ide/Plugin/Pragmas.hs b/plugins/default/src/Ide/Plugin/Pragmas.hs index 60219081e6..696f4c6d26 100644 --- a/plugins/default/src/Ide/Plugin/Pragmas.hs +++ b/plugins/default/src/Ide/Plugin/Pragmas.hs @@ -110,10 +110,10 @@ findPragma str = concatMap check possiblePragmas -- extension in an error message. possiblePragmas :: [T.Text] possiblePragmas = - [ name - | FlagSpec {flagSpecName = T.pack -> name} <- xFlags, - "Strict" /= name - ] + [ name + | FlagSpec{flagSpecName = T.pack -> name} <- xFlags + , "Strict" /= name + ] -- | All language pragmas, including the No- variants allPragmas :: [T.Text] From 298be740a607556e856c3da7abb30ae802f84761 Mon Sep 17 00:00:00 2001 From: Potato Hatsue <1793913507@qq.com> Date: Sun, 21 Feb 2021 13:40:18 +0800 Subject: [PATCH 4/9] Run pre-commit hook --- plugins/default/src/Ide/Plugin/Pragmas.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/default/src/Ide/Plugin/Pragmas.hs b/plugins/default/src/Ide/Plugin/Pragmas.hs index 696f4c6d26..826fc1dfa0 100644 --- a/plugins/default/src/Ide/Plugin/Pragmas.hs +++ b/plugins/default/src/Ide/Plugin/Pragmas.hs @@ -58,7 +58,7 @@ pragmaEditToAction :: Uri -> Range -> PragmaEdit -> (Command |? CodeAction) pragmaEditToAction uri range (title, p) = InR $ J.CodeAction title (Just J.CodeActionQuickFix) (Just (J.List [])) Nothing Nothing (Just edit) Nothing where - render (OptGHC x) = "{-# OPTIONS_GHC -Wno-" <> x <> " #-}\n" + render (OptGHC x) = "{-# OPTIONS_GHC -Wno-" <> x <> " #-}\n" render (LangExt x) = "{-# LANGUAGE " <> x <> " #-}\n" textEdits = J.List [J.TextEdit range $ render p] edit = From 46fe2a0784ddce309bafef26c6c2a875172aaa81 Mon Sep 17 00:00:00 2001 From: Potato Hatsue <1793913507@qq.com> Date: Sun, 21 Feb 2021 15:04:38 +0800 Subject: [PATCH 5/9] Fix and format class test --- test/functional/Class.hs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/functional/Class.hs b/test/functional/Class.hs index d8c925cdb6..4ab13b6511 100644 --- a/test/functional/Class.hs +++ b/test/functional/Class.hs @@ -1,26 +1,25 @@ -{-# LANGUAGE LambdaCase #-} --- {-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE TypeOperators #-} module Class ( tests ) where +import Control.Applicative.Combinators import Control.Lens hiding ((<.>)) -import Control.Monad.IO.Class (MonadIO(liftIO)) +import Control.Monad.IO.Class (MonadIO (liftIO)) import qualified Data.ByteString.Lazy as BS import qualified Data.Text.Encoding as T import Language.LSP.Test -import Language.LSP.Types hiding (_title, _command) -import qualified Language.LSP.Types.Lens as J +import Language.LSP.Types hiding (_command, _title) +import qualified Language.LSP.Types.Lens as J import System.FilePath import Test.Hls.Util import Test.Tasty import Test.Tasty.Golden import Test.Tasty.HUnit -import Control.Applicative.Combinators tests :: TestTree tests = testGroup @@ -34,6 +33,7 @@ tests = testGroup @?= [ Just "Add placeholders for '=='" , Just "Add placeholders for '/='" + , Just "Disable \"missing-methods\" warnings" ] , glodenTest "Creates a placeholder for '=='" "T1" "eq" $ \(eqAction:_) -> do From 4007428c3594c308a81c97338140aed2316cbacd Mon Sep 17 00:00:00 2001 From: Potato Hatsue <1793913507@qq.com> Date: Sun, 21 Feb 2021 16:53:08 +0800 Subject: [PATCH 6/9] Fix and format func-test (code action) --- test/functional/FunctionalCodeAction.hs | 29 +++++++++++++------------ 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/test/functional/FunctionalCodeAction.hs b/test/functional/FunctionalCodeAction.hs index 573a6423a7..131c9d87af 100644 --- a/test/functional/FunctionalCodeAction.hs +++ b/test/functional/FunctionalCodeAction.hs @@ -1,34 +1,35 @@ -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE CPP #-} -{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE CPP #-} {-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} module FunctionalCodeAction (tests) where import Control.Applicative.Combinators -import Control.Lens hiding (List) +import Control.Lens hiding (List) import Control.Monad import Control.Monad.IO.Class import Data.Aeson import Data.Default -import qualified Data.HashMap.Strict as HM +import qualified Data.HashMap.Strict as HM import Data.List import Data.Maybe -import qualified Data.Text as T +import qualified Data.Text as T import Ide.Plugin.Config -import Language.LSP.Test as Test +import Language.LSP.Test as Test import Language.LSP.Types -import qualified Language.LSP.Types.Lens as L import qualified Language.LSP.Types.Capabilities as C +import qualified Language.LSP.Types.Lens as L import Test.Hls.Util import Test.Hspec.Expectations +import System.FilePath (()) +import System.IO.Extra (withTempDir) import Test.Tasty -import Test.Tasty.ExpectedFailure (ignoreTestBecause, expectFailBecause) +import Test.Tasty.ExpectedFailure (expectFailBecause, + ignoreTestBecause) import Test.Tasty.HUnit -import System.FilePath (()) -import System.IO.Extra (withTempDir) {-# ANN module ("HLint: ignore Reduce duplication"::String) #-} @@ -517,8 +518,8 @@ missingPragmaTests = testGroup "missing pragma warning code actions" [ contents <- documentContents doc let expected = - [ "{-# LANGUAGE TypeApplications #-}" - , "{-# LANGUAGE ScopedTypeVariables #-}" + [ "{-# LANGUAGE ScopedTypeVariables #-}" + , "{-# LANGUAGE TypeApplications #-}" , "module TypeApplications where" , "" , "foo :: forall a. a -> a" From 7cdb788e0bd756b213324b67698e1332145902b6 Mon Sep 17 00:00:00 2001 From: Potato Hatsue <1793913507@qq.com> Date: Sun, 21 Feb 2021 18:45:37 +0800 Subject: [PATCH 7/9] Update test --- test/functional/FunctionalCodeAction.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/functional/FunctionalCodeAction.hs b/test/functional/FunctionalCodeAction.hs index 131c9d87af..468374b57f 100644 --- a/test/functional/FunctionalCodeAction.hs +++ b/test/functional/FunctionalCodeAction.hs @@ -518,8 +518,8 @@ missingPragmaTests = testGroup "missing pragma warning code actions" [ contents <- documentContents doc let expected = - [ "{-# LANGUAGE ScopedTypeVariables #-}" - , "{-# LANGUAGE TypeApplications #-}" + [ "{-# LANGUAGE TypeApplications #-}" + , "{-# LANGUAGE ScopedTypeVariables #-}" , "module TypeApplications where" , "" , "foo :: forall a. a -> a" From 88f01d34187b417543bc43273709dd4c95277844 Mon Sep 17 00:00:00 2001 From: Potato Hatsue <1793913507@qq.com> Date: Sun, 21 Feb 2021 21:53:45 +0800 Subject: [PATCH 8/9] Update test --- test/functional/FunctionalCodeAction.hs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/functional/FunctionalCodeAction.hs b/test/functional/FunctionalCodeAction.hs index 468374b57f..4cda16326a 100644 --- a/test/functional/FunctionalCodeAction.hs +++ b/test/functional/FunctionalCodeAction.hs @@ -518,8 +518,14 @@ missingPragmaTests = testGroup "missing pragma warning code actions" [ contents <- documentContents doc let expected = +-- TODO: Why CPP??? +#if __GLASGOW_HASKELL__ < 810 + [ "{-# LANGUAGE ScopedTypeVariables #-}" + , "{-# LANGUAGE TypeApplications #-}" +#else [ "{-# LANGUAGE TypeApplications #-}" , "{-# LANGUAGE ScopedTypeVariables #-}" +#endif , "module TypeApplications where" , "" , "foo :: forall a. a -> a" From eebf6b60ab0d934d77a7f24c225eb3f04fe21c7d Mon Sep 17 00:00:00 2001 From: Potato Hatsue <1793913507@qq.com> Date: Mon, 22 Feb 2021 13:16:34 +0800 Subject: [PATCH 9/9] Don't look contents in endOfModuleHeader --- plugins/default/src/Ide/Plugin/Pragmas.hs | 17 ++++++----------- test/functional/FunctionalCodeAction.hs | 2 +- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/plugins/default/src/Ide/Plugin/Pragmas.hs b/plugins/default/src/Ide/Plugin/Pragmas.hs index 826fc1dfa0..75c540d2d0 100644 --- a/plugins/default/src/Ide/Plugin/Pragmas.hs +++ b/plugins/default/src/Ide/Plugin/Pragmas.hs @@ -11,8 +11,8 @@ import Control.Lens hiding (List) import Control.Monad (join) import Control.Monad.IO.Class import qualified Data.HashMap.Strict as H -import Data.List.Extra (findIndex, nubOrdOn) -import Data.Maybe (catMaybes, fromMaybe, listToMaybe) +import Data.List.Extra (nubOrdOn) +import Data.Maybe (catMaybes, listToMaybe) import qualified Data.Text as T import Development.IDE as D import Development.IDE.GHC.Compat @@ -44,10 +44,9 @@ codeActionProvider :: PluginMethodHandler IdeState TextDocumentCodeAction codeActionProvider state _plId (CodeActionParams _ _ docId _ (J.CodeActionContext (J.List diags) _monly)) = do let mFile = docId ^. J.uri & uriToFilePath <&> toNormalizedFilePath' uri = docId ^. J.uri - content <- fmap VFS.virtualFileText <$> LSP.getVirtualFile (toNormalizedUri uri) pm <- liftIO $ fmap join $ runAction "Pragmas.GetParsedModule" state $ getParsedModule `traverse` mFile let dflags = ms_hspp_opts . pm_mod_summary <$> pm - insertRange = maybe (Range (Position 0 0) (Position 0 0)) (`endOfModuleHeader` content) pm + insertRange = maybe (Range (Position 0 0) (Position 0 0)) endOfModuleHeader pm pedits = nubOrdOn snd . concat $ suggest dflags <$> diags return $ Right $ List $ pragmaEditToAction uri insertRange <$> pedits @@ -179,16 +178,12 @@ completion _ide _ complParams = do -- | Find the first non-blank line before the first of (module name / imports / declarations). -- Useful for inserting pragmas. -endOfModuleHeader :: ParsedModule -> Maybe T.Text -> Range -endOfModuleHeader pm contents = +endOfModuleHeader :: ParsedModule -> Range +endOfModuleHeader pm = let mod = unLoc $ pm_parsed_source pm modNameLoc = getLoc <$> hsmodName mod firstImportLoc = getLoc <$> listToMaybe (hsmodImports mod) firstDeclLoc = getLoc <$> listToMaybe (hsmodDecls mod) - line = - fromMaybe 0 $ - firstNonBlankBefore . _line . _start =<< srcSpanToRange - =<< modNameLoc <|> firstImportLoc <|> firstDeclLoc - firstNonBlankBefore n = (n -) . fromMaybe 0 . findIndex (not . T.null) . reverse . take n . T.lines <$> contents + line = maybe 0 (_line . _start) (modNameLoc <|> firstImportLoc <|> firstDeclLoc >>= srcSpanToRange) loc = Position line 0 in Range loc loc diff --git a/test/functional/FunctionalCodeAction.hs b/test/functional/FunctionalCodeAction.hs index 4cda16326a..116843390a 100644 --- a/test/functional/FunctionalCodeAction.hs +++ b/test/functional/FunctionalCodeAction.hs @@ -592,9 +592,9 @@ disableWarningTests = ] , T.unlines [ "{-# OPTIONS_GHC -Wall #-}" - , "{-# OPTIONS_GHC -Wno-unused-imports #-}" , "" , "" + , "{-# OPTIONS_GHC -Wno-unused-imports #-}" , "module M where" , "" , "import Data.Functor"