1
0
mirror of https://github.com/IHaskell/IHaskell.git synced 2025-04-20 05:16:09 +00:00

add lint module and lint module tests

This commit is contained in:
Andrew Gibiansky 2016-09-11 11:00:51 -07:00
parent 8fbd2a5653
commit 15e989ef4d
4 changed files with 347 additions and 2 deletions
ihaskell.cabal
src/Jupyter/IHaskell
tests
Jupyter/IHaskell/Test
Test.hs

@ -39,12 +39,15 @@ library
directory >= 1.2,
megaparsec,
semigroups,
haskell-src-exts,
hlint,
extra
exposed-modules:
Jupyter.IHaskell.Complete
Jupyter.IHaskell.Evaluate
Jupyter.IHaskell.Interpreter
Jupyter.IHaskell.Parser
Jupyter.IHaskell.Lint
if flag(binPkgDb)
build-depends: bin-package-db
@ -70,6 +73,7 @@ Test-Suite test-ihaskell
other-modules:
Jupyter.IHaskell.Test.Complete
Jupyter.IHaskell.Test.Parser
Jupyter.IHaskell.Test.Lint
hs-source-dirs: tests
default-language: Haskell2010
build-depends:

@ -0,0 +1,240 @@
{-|
Module : Jupyter.IHaskell.Lint
Description : IHaskell interface to HLint
Copyright : (c) Andrew Gibiansky, 2016
License : MIT
Maintainer : andrew.gibiansky@gmail.com
Stability : stable
Portability : POSIX
-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Jupyter.IHaskell.Lint (
-- * Running @hlint@
lintCodeBlock,
LintSuggestion(..),
SuggestionType(..),
-- * Rendering @hlint@ suggestions
renderSuggestionPlain,
renderSuggestionHTML,
) where
-- Imports from 'base'
import Control.Concurrent.MVar (putMVar, newEmptyMVar, isEmptyMVar, readMVar, MVar)
import Control.Monad (unless)
import Data.Maybe (mapMaybe, fromJust)
import System.IO.Unsafe (unsafePerformIO)
-- Imports from 'text'
import Data.Text (Text)
import qualified Data.Text as T
-- Imports from 'haskell-src-exts'
import Language.Haskell.Exts.Annotated (parseFileContentsWithMode, parseExpWithMode,
parseModuleWithMode, parseStmtWithMode, ParseMode,
ParseResult(..), Decl(..), Module(..), Stmt(..),
SrcSpanInfo(..), SrcSpan(..))
import Language.Haskell.Exts.Annotated.Build (doE)
-- Imports from 'hlint'
import Language.Haskell.HLint2 (ParseFlags(..), Classify, Hint, applyHints,
autoSettings, Severity(..), Idea(..))
-- Imports from 'ihaskell'
import Jupyter.IHaskell.Parser (Loc(..), CodeBlock(..))
-- | A suggestion output by @hlint@.
data LintSuggestion =
LintSuggestion
{ lintSuggestionLine :: Int -- ^ Line on which suggestion applies
, lintSuggestionFound :: Text -- ^ Code before applying suggestion
, lintSuggestionWhyNot :: Text -- ^ Code after applying suggestion
, lintSuggestionType :: SuggestionType -- ^ Suggestion severity
, lintSuggestion :: Text -- ^ Description of the suggestion transformation
}
deriving (Eq, Ord, Show)
-- | Suggestion type, indicating how serious an @hlint@ suggestion is (e.g. whether it is just
-- stylistic or semantically important).
data SuggestionType = InfoSuggestion -- ^ Default suggestion type
| WarningSuggestion -- ^ More serious suggestion (warning)
| ErrorSuggestion -- ^ Suggestion that should be an error
deriving (Eq, Ord, Show)
-- | Global 'MVar' to store settings for Hlint once it's initialized. Since @hlint@ reads files from
-- disk, we store linter data globally after the first time it's been read.
hlintSettings :: MVar (ParseFlags, [Classify], Hint)
hlintSettings = unsafePerformIO newEmptyMVar
{-# NOINLINE hlintSettings #-}
-- | Identifier used when one is needed for proper context. This allows us to pass fully formed
-- modules even when the input is not a full module (e.g a statement), because we can use this
-- identifier assuming that the code does not have it, and then remove it in postprocessing.
lintIdent :: Text
lintIdent = "lintIdentAEjlkQeh"
-- | Given a parsed code chunk, perform linting and output a list of suggestions for the chunk.
lintCodeBlock :: Loc CodeBlock -> IO [LintSuggestion]
lintCodeBlock block = do
-- Initialize hlint settings the first time hlint is used.
initialized <- not <$> isEmptyMVar hlintSettings
unless initialized $
autoSettings >>= putMVar hlintSettings
(flags, classify, hint) <- readMVar hlintSettings
return $
case createModule (hseFlags flags) block of
Nothing -> []
Just modl -> mapMaybe ideaToSuggestion $ applyHints classify hint [(modl, [])]
-- | Convert from an @hlint@ 'Idea' to a @ihaskell@ 'LintSuggestion'. Doing this conversion allows
-- this module to export a stable interface, even if the underlying @hlint@ interface changes.
ideaToSuggestion :: Idea -> Maybe LintSuggestion
ideaToSuggestion idea =
case ideaTo idea of
Nothing -> Nothing
Just whyNot ->
Just
LintSuggestion
{ lintSuggestionLine = srcSpanStartLine $ ideaSpan idea
, lintSuggestionFound = preprocessSuggestionText $ T.pack $ ideaFrom idea
, lintSuggestionWhyNot = preprocessSuggestionText $ T.pack whyNot
, lintSuggestionType = severityType $ ideaSeverity idea
, lintSuggestion = T.pack $ ideaHint idea
}
where
severityType sev =
case sev of
Error -> ErrorSuggestion
Warning -> WarningSuggestion
_ -> InfoSuggestion
preprocessSuggestionText :: Text -> Text
preprocessSuggestionText = T.replace lintIdent "" . dropLeadingDo
-- Drop leading ' do ', and blank spaces following.
dropLeadingDo :: Text -> Text
dropLeadingDo string =
-- If this is not a statement, we don't need to drop the do statement.
if lintIdent `T.isInfixOf` string
then T.unlines . clean . T.lines $ string
else string
-- Remove 'do' blocks. If the first line starts with a `do`... Note that hlint always indents by two
-- spaces in its output.
clean :: [Text] -> [Text]
clean stmtLines =
case stmtLines of
[] -> []
l:ls ->
case T.stripPrefix " do" l of
Just l' ->
case span (T.isPrefixOf " ") ls of
(indented, next) ->
l' : map (fromJust . T.stripPrefix " ") indented ++ clean next
Nothing -> l : clean ls
-- | Create a parsed @haskell-src-exts@ module from a code block. This module can then be processed
-- by @hlint@.
createModule :: ParseMode -> Loc CodeBlock -> Maybe (Module SrcSpanInfo)
createModule mode (Loc line block) =
case block of
Expression expr -> parseResultToMaybe $ exprToModule $ T.unpack expr
Declarations decl -> parseResultToMaybe $ declToModule $ T.unpack decl
Statement stmt -> parseResultToMaybe $ stmtToModule $ T.unpack stmt
Import impt -> parseResultToMaybe $ imptToModule $ T.unpack impt
_ -> Nothing
where
parseResultToMaybe :: ParseResult a -> Maybe a
parseResultToMaybe (ParseOk a) = Just a
parseResultToMaybe _ = Nothing
srcSpan :: String -> SrcSpanInfo
srcSpan blk =
SrcSpanInfo
(SrcSpan
{ srcSpanFilename = "<interactive>"
, srcSpanStartLine = line
, srcSpanStartColumn = 0
, srcSpanEndLine = line + length (lines blk)
, srcSpanEndColumn = if null blk
then 0
else length (lines blk)
})
[]
moduleWithDecls :: String -> Decl SrcSpanInfo -> (Module SrcSpanInfo)
moduleWithDecls blk decl = Module (srcSpan blk) Nothing [] [] [decl]
declToModule :: String -> ParseResult (Module SrcSpanInfo)
declToModule decl = parseModuleWithMode mode decl
exprToModule :: String -> ParseResult (Module SrcSpanInfo)
exprToModule expr = moduleWithDecls expr <$>
SpliceDecl (srcSpan expr) <$>
parseExpWithMode mode expr
-- Convert a statement to a module by turning it into a template haskell splice, which consists of a
-- do block with the statement followed by a return statement.
stmtToModule :: String -> ParseResult (Module SrcSpanInfo)
stmtToModule stmt =
case parseStmtWithMode mode stmt of
ParseOk _ ->
ParseOk $ moduleWithDecls stmt $
SpliceDecl (srcSpan stmt) $
let fromParseOk = fromJust . parseResultToMaybe
in doE (srcSpan stmt)
[ fromParseOk (parseStmtWithMode mode stmt)
, fromParseOk
(Qualifier (srcSpan stmt) <$> parseExpWithMode mode (T.unpack lintIdent))
]
ParseFailed a b -> ParseFailed a b
imptToModule :: String -> ParseResult (Module SrcSpanInfo)
imptToModule = parseFileContentsWithMode mode
-- | Render a suggestion as plain text.
renderSuggestionPlain :: LintSuggestion -> Text
renderSuggestionPlain LintSuggestion { .. } =
T.concat
[ "Line "
, T.pack $ show lintSuggestionLine
, ": "
, lintSuggestion
, "\nFound:\n"
, lintSuggestionFound
, "\nWhy not:\n"
, lintSuggestionWhyNot
]
-- | Render a suggestion as HTML, for display in the IHaskell notebook interface.
renderSuggestionHTML :: LintSuggestion -> Text
renderSuggestionHTML LintSuggestion { .. } =
T.concat
[ named lintSuggestion
, floating "left" $ style severityClassColor "Found:"
, lintSuggestionFound
, floating "left" $ style severityClassColor "Why Not:"
, lintSuggestionWhyNot
]
where
severityClassColor =
case lintSuggestionType of
ErrorSuggestion -> "red"
WarningSuggestion -> "rgb(200, 130, 0)"
InfoSuggestion -> "blue"
style :: Text -> Text -> Text
style color content =
T.concat ["<div style=\"font-weight: bold; color: ", color, ";\">", content, "</div>"]
named :: Text -> Text
named content =
T.concat ["<div style=\"font-weight: bold;\" style=\"clear:both;\">", content, "</div>"]
floating :: Text -> Text -> Text
floating float content =
T.concat ["<div style=\"float: ", float, ";\">", content, "</div>"]

@ -0,0 +1,100 @@
{-|
Module : Jupyter.IHaskell.Test.Lint
Description : Tests for Jupyter.IHaskell.Lint
Copyright : (c) Andrew Gibiansky, 2016
License : MIT
Maintainer : andrew.gibiansky@gmail.com
Stability : stable
Portability : POSIX
-}
{-# LANGUAGE OverloadedStrings #-}
module Jupyter.IHaskell.Test.Lint (lintTests) where
-- Imports from 'base'
import Control.Monad (unless)
-- Imports from 'tasty'
import Test.Tasty (TestTree)
-- Imports from 'tasty-hunit'
import Test.Tasty.HUnit (testCase, assertFailure)
-- Imports from 'transformers'
import Control.Monad.IO.Class (MonadIO(..))
-- Imports from 'ihaskell'
import Jupyter.IHaskell.Lint (lintCodeBlock, LintSuggestion(..), SuggestionType(..))
import Jupyter.IHaskell.Parser (Loc(..), CodeBlock(..))
-- | Test the IHaskell HLint integration.
lintTests :: TestTree
lintTests = testCase "Linting" $ do
-- Basic expression linting.
Expression "(f x)" --> [ LintSuggestion
{ lintSuggestionLine = 1
, lintSuggestionFound = "(f x)"
, lintSuggestionWhyNot = "f x"
, lintSuggestionType = InfoSuggestion
, lintSuggestion = "Redundant bracket"
}
]
Expression "f (f $ x)" --> [ LintSuggestion
{ lintSuggestionLine = 1
, lintSuggestionFound = "f $ x"
, lintSuggestionWhyNot = "f x"
, lintSuggestionType = InfoSuggestion
, lintSuggestion = "Redundant $"
}
]
-- Basic declaration linting.
Declarations "x = f (f $ x)" --> [ LintSuggestion
{ lintSuggestionLine = 1
, lintSuggestionFound = "f $ x"
, lintSuggestionWhyNot = "f x"
, lintSuggestionType = InfoSuggestion
, lintSuggestion = "Redundant $"
}
]
-- Linting multiple declarations at once.
Declarations "x = f (f $ x)\nx = f (f $ x)" --> [ LintSuggestion
{ lintSuggestionLine = 1
, lintSuggestionFound = "f $ x"
, lintSuggestionWhyNot = "f x"
, lintSuggestionType = InfoSuggestion
, lintSuggestion = "Redundant $"
}
, LintSuggestion
{ lintSuggestionLine = 2
, lintSuggestionFound = "f $ x"
, lintSuggestionWhyNot = "f x"
, lintSuggestionType = InfoSuggestion
, lintSuggestion = "Redundant $"
}
]
Statement "x <- f (f $ x)" --> [ LintSuggestion
{ lintSuggestionLine = 1
, lintSuggestionFound = "f $ x"
, lintSuggestionWhyNot = "f x"
, lintSuggestionType = InfoSuggestion
, lintSuggestion = "Redundant $"
}
]
-- | Run a single linter test. Lint the input and then compare output suggestions to generated
-- suggestions.
(-->) :: CodeBlock -> [LintSuggestion] -> IO ()
block --> expected = do
observed <- lintCodeBlock (Loc 1 block)
unless (and (zipWith (==) expected observed) && length expected == length observed) $
liftIO $ assertFailure $
concat
[ "Did not get expected suggestions for: "
, show block
, "\nExpected: "
, show expected
, "\nObserved: "
, show observed
]

@ -3,12 +3,13 @@ module Main (main) where
-- Imports from 'tasty'
import Test.Tasty (defaultMain, testGroup)
-- Imports from 'jupyter'
-- Imports from 'ihaskell'
import Jupyter.IHaskell.Test.Complete (completionTests)
import Jupyter.IHaskell.Test.Lint (lintTests)
import Jupyter.IHaskell.Test.Parser (parserTests)
-- | Run all Haskell tests for the @ihaskell@ library package.
main :: IO ()
main =
defaultMain $
testGroup "Tests" [parserTests, completionTests]
testGroup "Tests" [parserTests, completionTests, lintTests]