packages feed

hadolint 1.4.0 → 1.5.0

raw patch · 10 files changed

+430/−84 lines, 10 filesdep +aesondep +dlistdep +textdep ~bytestringPVP ok

version bump matches the API change (PVP)

Dependencies added: aeson, dlist, text

Dependency ranges changed: bytestring

API changes (from Hackage documentation)

- Hadolint.Formatter: formatCheck :: RuleCheck -> String
- Hadolint.Formatter: formatError :: ParseError -> String
+ Hadolint.Formatter.Checkstyle: formatResult :: Result -> Builder
+ Hadolint.Formatter.Checkstyle: printResult :: Result -> IO ()
+ Hadolint.Formatter.Codeclimate: formatResult :: Result -> DList Issue
+ Hadolint.Formatter.Codeclimate: instance Data.Aeson.Types.ToJSON.ToJSON Hadolint.Formatter.Codeclimate.Issue
+ Hadolint.Formatter.Codeclimate: instance Data.Aeson.Types.ToJSON.ToJSON Hadolint.Formatter.Codeclimate.Location
+ Hadolint.Formatter.Codeclimate: instance Data.Aeson.Types.ToJSON.ToJSON Hadolint.Formatter.Codeclimate.Pos
+ Hadolint.Formatter.Codeclimate: instance GHC.Generics.Generic Hadolint.Formatter.Codeclimate.Pos
+ Hadolint.Formatter.Codeclimate: printResult :: Result -> IO ()
+ Hadolint.Formatter.Format: Result :: DList ParseError -> DList RuleCheck -> Result
+ Hadolint.Formatter.Format: [checks] :: Result -> DList RuleCheck
+ Hadolint.Formatter.Format: [errors] :: Result -> DList ParseError
+ Hadolint.Formatter.Format: data Result
+ Hadolint.Formatter.Format: formatErrorReason :: ParseError -> String
+ Hadolint.Formatter.Format: instance GHC.Base.Monoid Hadolint.Formatter.Format.Result
+ Hadolint.Formatter.Format: instance GHC.Classes.Eq Hadolint.Formatter.Format.Result
+ Hadolint.Formatter.Format: severityText :: Severity -> String
+ Hadolint.Formatter.Format: stripNewlines :: String -> String
+ Hadolint.Formatter.Format: toResult :: Either ParseError [RuleCheck] -> Result
+ Hadolint.Formatter.Json: formatResult :: Result -> Value
+ Hadolint.Formatter.Json: instance Data.Aeson.Types.ToJSON.ToJSON Hadolint.Formatter.Json.JsonFormat
+ Hadolint.Formatter.Json: printResult :: Result -> IO ()
+ Hadolint.Formatter.TTY: formatError :: ParseError -> String
+ Hadolint.Formatter.TTY: printResult :: Result -> IO ()

Files

app/Main.hs view
@@ -1,18 +1,17 @@ {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE NamedFieldPuns #-}  module Main where -import Hadolint.Formatter import Hadolint.Rules import Language.Docker.Parser import Language.Docker.Syntax  import Control.Applicative import Control.Monad (filterM)-import Data.List (sort) import Data.Maybe (listToMaybe)-import Data.Semigroup+import Data.Semigroup ((<>)) import qualified Data.Version as V (showVersion) import qualified Data.Yaml as Yaml import Development.GitRev (gitDescribe)@@ -22,15 +21,28 @@ import System.Directory        (XdgDirectory(..), doesFileExist, getCurrentDirectory,         getXdgDirectory)-import System.Environment (getArgs)-import System.Exit hiding (die)+import System.Exit (exitFailure, exitSuccess) import System.FilePath ((</>)) import Text.Parsec (ParseError) +import qualified Hadolint.Formatter.Checkstyle as Checkstyle+import qualified Hadolint.Formatter.Codeclimate as Codeclimate+import Hadolint.Formatter.Format (toResult)+import qualified Hadolint.Formatter.Json as Json+import qualified Hadolint.Formatter.TTY as TTY+ type IgnoreRule = String +data OutputFormat+    = Json+    | TTY+    | CodeclimateJson+    | Checkstyle+    deriving (Show, Eq)+ data LintOptions = LintOptions     { showVersion :: Bool+    , format :: OutputFormat     , ignoreRules :: [IgnoreRule]     , dockerfiles :: [String]     } deriving (Show)@@ -44,22 +56,49 @@ ignoreFilter :: [IgnoreRule] -> RuleCheck -> Bool ignoreFilter ignoredRules (RuleCheck (Metadata code _ _) _ _ _) = code `notElem` ignoredRules -printChecks :: [RuleCheck] -> IO ()-printChecks checks = do-    mapM_ (putStrLn . formatCheck) $ sort checks-    if null checks-        then exitSuccess-        else die+toOutputFormat :: String -> Maybe OutputFormat+toOutputFormat "json" = Just Json+toOutputFormat "tty" = Just TTY+toOutputFormat "codeclimate" = Just CodeclimateJson+toOutputFormat "checkstyle" = Just Checkstyle+toOutputFormat _ = Nothing +showFormat :: OutputFormat -> String+showFormat Json = "json"+showFormat TTY = "tty"+showFormat CodeclimateJson = "codeclimate"+showFormat Checkstyle = "checkstyle"+ parseOptions :: Parser LintOptions parseOptions =     LintOptions <$> -- CLI options parser definition-    switch (long "version" <> short 'v' <> help "Show version") <*>-    many-        (strOption-             (long "ignore" <> help "Ignore rule. If present, config file is ignored" <>-              metavar "RULECODE")) <*>-    many (argument str (metavar "DOCKERFILE..."))+    version <*>+    outputFormat <*>+    ignoreList <*>+    files+  where+    version = switch (long "version" <> short 'v' <> help "Show version")+    --+    -- | Parse the output format option+    outputFormat =+        option+            (maybeReader toOutputFormat)+            (long "format" <> -- options for the output format+             short 'f' <>+             help "The output format for the results [tty | json | checkstyle | codeclimate]" <>+             value TTY <> -- The default value+             showDefaultWith showFormat <>+             completeWith ["tty", "json", "checkstyle", "codeclimate"])+    --+    -- | Parse a list of ignored rules+    ignoreList =+        many+            (strOption+                 (long "ignore" <> help "Ignore rule. If present, config file is ignored" <>+                  metavar "RULECODE"))+    --+    -- | Parse a list of dockerfile names+    files = many (argument str (metavar "DOCKERFILE..." <> action "file"))  main :: IO () main = execParser opts >>= applyConfig >>= lint@@ -71,7 +110,7 @@              header "hadolint - Dockerfile Linter written in Haskell")  applyConfig :: LintOptions -> IO LintOptions-applyConfig o@(LintOptions _ (_:_) _) = return o+applyConfig o@LintOptions {ignoreRules = (_:_)} = return o applyConfig o = do     theConfig <- findConfig     case theConfig of@@ -105,10 +144,14 @@ parseFilename "-" = "/dev/stdin" parseFilename s = s -lintDockerfile :: [IgnoreRule] -> String -> IO ()+lintDockerfile :: [IgnoreRule] -> String -> IO (Either ParseError [RuleCheck]) lintDockerfile ignoreRules dockerfile = do     ast <- parseFile $ parseFilename dockerfile-    checkAst (ignoreFilter ignoreRules) ast+    return (processedFile ast)+  where+    processedFile = fmap processRules+    processRules dockerfile = filter ignored (analyzeAll dockerfile)+    ignored = ignoreFilter ignoreRules  getVersion :: String getVersion@@ -117,15 +160,25 @@     | otherwise = "Haskell Dockerfile Linter " ++ $(gitDescribe)  lint :: LintOptions -> IO ()-lint (LintOptions True _ _) = putStrLn getVersion >> exitSuccess-lint (LintOptions _ _ []) = putStrLn "Please provide a Dockerfile" >> exitFailure-lint (LintOptions _ ignore dfiles) = mapM_ (lintDockerfile ignore) dfiles--checkAst :: (RuleCheck -> Bool) -> Either ParseError Dockerfile -> IO ()-checkAst checkFilter ast =-    case ast of-        Left err -> putStrLn (formatError err) >> exitFailure-        Right dockerfile -> printChecks $ filter checkFilter $ analyzeAll dockerfile+lint LintOptions {showVersion = True} = putStrLn getVersion >> exitSuccess+lint LintOptions {dockerfiles = []} = putStrLn "Please provide a Dockerfile" >> exitFailure+lint LintOptions {ignoreRules = ignoreList, dockerfiles = dFiles, format} = do+    processedFiles <- mapM (lintDockerfile ignoreList) dFiles+    let allResults = results processedFiles+    printResult allResults+    if allResults /= mempty+        then exitFailure+        else exitSuccess+  where+    results = foldMap toResult -- Parse and check rules for each dockerfile,+                               -- then convert them to a Result and combine with+                               -- the result of the previous dockerfile results+    printResult res =+        case format of+            TTY -> TTY.printResult res+            Json -> Json.printResult res+            Checkstyle -> Checkstyle.printResult res+            CodeclimateJson -> Codeclimate.printResult res >> exitSuccess  analyzeAll :: Dockerfile -> [RuleCheck] analyzeAll = analyze rules@@ -134,6 +187,3 @@ analyzeEither :: Either t Dockerfile -> [RuleCheck] analyzeEither (Left err) = [] analyzeEither (Right dockerfile) = analyzeAll dockerfile--die :: IO a-die = exitWith (ExitFailure 1)
hadolint.cabal view
@@ -2,10 +2,10 @@ -- -- see: https://github.com/sol/hpack ----- hash: 27e3b32590f30c0792f229a5aebdd2796f20c9ef9889029a1f2c6dbd2d986bcf+-- hash: 0e73d59878f7e4b0a47b9ea2607d8a8de24ade73e92179f9c23870e7e0364c35  name:           hadolint-version:        1.4.0+version:        1.5.0 synopsis:       Dockerfile Linter JavaScript API description:    A smarter Dockerfile linter that helps you build best practice Docker images. category:       Development@@ -30,15 +30,22 @@       src   build-depends:       ShellCheck >=0.4.7+    , aeson     , base >=4.8 && <5-    , bytestring >=0.10+    , bytestring+    , dlist     , language-docker >=2.0.0     , parsec >=3.1     , split >=0.2+    , text   exposed-modules:       Hadolint.Bash-      Hadolint.Formatter       Hadolint.Rules+      Hadolint.Formatter.Format+      Hadolint.Formatter.Json+      Hadolint.Formatter.TTY+      Hadolint.Formatter.Codeclimate+      Hadolint.Formatter.Checkstyle   other-modules:       Paths_hadolint   default-language: Haskell2010@@ -71,6 +78,7 @@   build-depends:       HUnit >=1.2     , ShellCheck >=0.4.7+    , aeson     , base >=4.8 && <5     , bytestring >=0.10     , hadolint
− src/Hadolint/Formatter.hs
@@ -1,43 +0,0 @@-module Hadolint.Formatter-    ( formatCheck-    , formatError-    ) where--import Control.Monad-import Data.Char (isSpace)-import Hadolint.Rules-import Language.Docker.Syntax-import Text.Parsec.Error-       (Message, ParseError, errorMessages, errorPos, messageString,-        showErrorMessages)-import Text.Parsec.Pos--formatError :: ParseError -> String-formatError err = posPart ++ stripNewlines msgPart-  where-    pos = errorPos err-    posPart = sourceName pos ++ ":" ++ show (sourceLine pos) ++ ":" ++ show (sourceColumn pos)-    msgPart =-        showErrorMessages-            "or"-            "unknown parse error"-            "expecting"-            "unexpected"-            "end of input"-            (errorMessages err)-    stripNewlines =-        map-            (\c ->-                 if c == '\n'-                     then ' '-                     else c)--formatCheck :: RuleCheck -> String-formatCheck (RuleCheck metadata source linenumber _) =-    formatPos source linenumber ++ code metadata ++ " " ++ message metadata--formatPos :: Filename -> Linenumber -> String-formatPos source linenumber =-    if linenumber >= 0-        then source ++ ":" ++ show linenumber ++ " "-        else source ++ " "
+ src/Hadolint/Formatter/Checkstyle.hs view
@@ -0,0 +1,91 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE OverloadedStrings #-}++module Hadolint.Formatter.Checkstyle+    ( printResult+    , formatResult+    ) where++import qualified Data.ByteString.Builder as Builder+import qualified Data.ByteString.Lazy.Char8 as B+import Data.Char+import Data.DList (toList)+import Data.List (groupBy)+import Data.Monoid ((<>), mconcat)+import Hadolint.Formatter.Format+import Hadolint.Rules (Metadata(..), RuleCheck(..))+import ShellCheck.Interface+import Text.Parsec (ParseError)+import Text.Parsec.Error (ParseError, errorPos)+import Text.Parsec.Pos++data CheckStyle = CheckStyle+    { file :: String+    , line :: Int+    , column :: Int+    , impact :: String+    , msg :: String+    , source :: String+    }++errorToCheckStyle :: ParseError -> CheckStyle+errorToCheckStyle err =+    CheckStyle+    { file = sourceName pos+    , line = sourceLine pos+    , column = sourceColumn pos+    , impact = severityText ErrorC+    , msg = stripNewlines (formatErrorReason err)+    , source = "DL1000"+    }+  where+    pos = errorPos err++ruleToCheckStyle :: RuleCheck -> CheckStyle+ruleToCheckStyle RuleCheck {..} =+    CheckStyle+    { file = filename+    , line = linenumber+    , column = 1+    , impact = severityText (severity metadata)+    , msg = message metadata+    , source = code metadata+    }++toXml :: [CheckStyle] -> Builder.Builder+toXml checks = wrap fileName (foldMap convert checks)+  where+    wrap name innerNode = "<file " <> attr "name" name <> ">" <> innerNode <> "</file>"+    convert CheckStyle {..} =+        "<error " <> -- Beging the node construction+        attr "line" (show line) <>+        attr "column" (show column) <>+        attr "severity" impact <>+        attr "message" msg <>+        attr "source" source <>+        "/>"+    fileName = file (head checks)++attr name value = Builder.string8 name <> "='" <> Builder.string8 (escape value) <> "' "++escape = concatMap doEscape+  where+    doEscape c =+        if isOk c+            then [c]+            else "&#" ++ show (ord c) ++ ";"+    isOk x = any ($x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` [' ', '.', '/'])]++formatResult :: Result -> Builder.Builder+formatResult (Result errors checks) =+    "<?xml version='1.0' encoding='UTF-8'?><checkstyle version='4.3'>" <> xmlBody <> "</checkstyle>"+  where+    xmlBody = mconcat xmlChunks+    xmlChunks = fmap toXml (groupBy sameFileName flatten)+    flatten = toList $ checkstyleErrors <> checkstyleChecks+    checkstyleErrors = fmap errorToCheckStyle errors+    checkstyleChecks = fmap ruleToCheckStyle checks+    sameFileName CheckStyle {file = f1} CheckStyle {file = f2} = f1 == f2++printResult :: Result -> IO ()+printResult result = B.putStr (Builder.toLazyByteString (formatResult result))
+ src/Hadolint/Formatter/Codeclimate.hs view
@@ -0,0 +1,95 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}++module Hadolint.Formatter.Codeclimate+    ( printResult+    , formatResult+    ) where++import Data.Aeson hiding (Result)+import qualified Data.ByteString.Lazy as B+import Data.DList (DList)+import Data.Monoid ((<>))+import GHC.Generics+import Hadolint.Formatter.Format (Result(..), formatErrorReason)+import Hadolint.Rules (Metadata(..), RuleCheck(..))+import ShellCheck.Interface+import Text.Parsec.Error (errorPos)+import Text.Parsec.Pos++data Issue = Issue+    { checkName :: String+    , description :: String+    , location :: Location+    , impact :: String+    }++data Location+    = LocLine String+              Int+    | LocPos String+             Pos++instance ToJSON Location where+    toJSON (LocLine path l) = object ["path" .= path, "lines" .= object ["begin" .= l, "end" .= l]]+    toJSON (LocPos path pos) =+        object ["path" .= path, "positions" .= object ["begin" .= pos, "end" .= pos]]++data Pos = Pos+    { line :: Int+    , column :: Int+    } deriving (Generic)++instance ToJSON Pos++instance ToJSON Issue where+    toJSON Issue {..} =+        object+            [ "type" .= ("issue" :: String)+            , "check_name" .= checkName+            , "description" .= description+            , "categories" .= (["Bug Risk"] :: [String])+            , "location" .= location+            , "severity" .= impact+            ]++errorToIssue err =+    Issue+    { checkName = "DL1000"+    , description = formatErrorReason err+    , location = LocPos (sourceName pos) (Pos (sourceLine pos) (sourceColumn pos))+    , impact = severityText ErrorC+    }+  where+    pos = errorPos err++checkToIssue RuleCheck {..} =+    Issue+    { checkName = code metadata+    , description = message metadata+    , location = LocLine filename linenumber+    , impact = severityText (severity metadata)+    }++severityText :: Severity -> String+severityText severity =+    case severity of+        ErrorC -> "blocker"+        WarningC -> "major"+        InfoC -> "info"+        StyleC -> "minor"++formatResult :: Result -> DList Issue+formatResult (Result errors checks) = allIssues+  where+    allIssues = errorMessages <> checkMessages+    errorMessages = fmap errorToIssue errors+    checkMessages = fmap checkToIssue checks++printResult :: Result -> IO ()+printResult result = mapM_ output (formatResult result)+  where+    output value = do+        B.putStr (encode value)+        B.putStr (B.singleton 0x00)
+ src/Hadolint/Formatter/Format.hs view
@@ -0,0 +1,56 @@+module Hadolint.Formatter.Format+    ( formatErrorReason+    , severityText+    , stripNewlines+    , Result(..)+    , toResult+    ) where++import Data.DList (DList, fromList, singleton)+import Data.List (sort)+import Data.Monoid+import Hadolint.Rules+import ShellCheck.Interface+import Text.Parsec.Error+       (ParseError, errorMessages, showErrorMessages)++data Result = Result+    { errors :: DList ParseError+    , checks :: DList RuleCheck+    } deriving (Eq)++instance Monoid Result where+    mempty = Result mempty mempty+    mappend (Result e1 c1) (Result e2 c2) = Result (e1 <> e2) (c1 <> c2)++toResult :: Either ParseError [RuleCheck] -> Result+toResult res =+    case res of+        Left err -> Result (singleton err) mempty+        Right c -> Result mempty (fromList (sort c))++severityText :: Severity -> String+severityText severity =+    case severity of+        ErrorC -> "error"+        WarningC -> "warning"+        InfoC -> "info"+        StyleC -> "style"++formatErrorReason :: ParseError -> String+formatErrorReason err =+    showErrorMessages+        "or"+        "unknown parse error"+        "expecting"+        "unexpected"+        "end of input"+        (errorMessages err)++stripNewlines :: String -> String+stripNewlines =+    map+        (\c ->+             if c == '\n'+                 then ' '+                 else c)
+ src/Hadolint/Formatter/Json.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE OverloadedStrings #-}++module Hadolint.Formatter.Json+    ( printResult+    , formatResult+    ) where++import Data.Aeson hiding (Result)+import qualified Data.ByteString.Lazy.Char8 as B+import Data.Monoid ((<>))+import Hadolint.Formatter.Format+       (Result(..), formatErrorReason, severityText)+import Hadolint.Rules (Metadata(..), RuleCheck(..))+import ShellCheck.Interface+import Text.Parsec (ParseError)+import Text.Parsec.Error (ParseError, errorPos)+import Text.Parsec.Pos++data JsonFormat+    = JsonCheck RuleCheck+    | JsonParseError ParseError++instance ToJSON JsonFormat where+    toJSON (JsonCheck RuleCheck {..}) =+        object+            [ "file" .= filename+            , "line" .= linenumber+            , "column" .= (1 :: Int)+            , "level" .= severityText (severity metadata)+            , "code" .= code metadata+            , "message" .= message metadata+            ]+    toJSON (JsonParseError err) =+        object+            [ "file" .= sourceName pos+            , "line" .= sourceLine pos+            , "column" .= sourceColumn pos+            , "level" .= severityText ErrorC+            , "code" .= ("DL1000" :: String)+            , "message" .= formatErrorReason err+            ]+      where+        pos = errorPos err++formatResult :: Result -> Value+formatResult (Result errors checks) = toJSON allMessages+  where+    allMessages = errorMessages <> checkMessages+    errorMessages = fmap JsonParseError errors+    checkMessages = fmap JsonCheck checks++printResult :: Result -> IO ()+printResult result = B.putStrLn (encode (formatResult result))
+ src/Hadolint/Formatter/TTY.hs view
@@ -0,0 +1,37 @@+module Hadolint.Formatter.TTY+    ( printResult+    , formatError+    ) where++import Hadolint.Formatter.Format+import Hadolint.Rules+import Language.Docker.Syntax+import Text.Parsec.Error (ParseError, errorPos)+import Text.Parsec.Pos++formatErrors :: Functor t => t ParseError -> t String+formatErrors = fmap formatError++formatError :: ParseError -> String+formatError err = posPart ++ stripNewlines (formatErrorReason err)+  where+    pos = errorPos err+    posPart = sourceName pos ++ ":" ++ show (sourceLine pos) ++ ":" ++ show (sourceColumn pos)++formatChecks :: Functor t => t RuleCheck -> t String+formatChecks = fmap formatCheck+  where+    formatCheck (RuleCheck metadata source linenumber _) =+        formatPos source linenumber ++ code metadata ++ " " ++ message metadata++formatPos :: Filename -> Linenumber -> String+formatPos source linenumber =+    if linenumber >= 0+        then source ++ ":" ++ show linenumber ++ " "+        else source ++ " "++printResult :: Result -> IO ()+printResult (Result errors checks) = printErrors >> printChecks+  where+    printErrors = mapM_ putStrLn (formatErrors errors)+    printChecks = mapM_ putStrLn (formatChecks checks)
src/Hadolint/Rules.hs view
@@ -1,11 +1,9 @@ module Hadolint.Rules where  import Control.Arrow ((&&&))-import Data.List-       (intercalate, isInfixOf, isPrefixOf, isSuffixOf, mapAccumL)+import Data.List (isInfixOf, isPrefixOf, isSuffixOf, mapAccumL) import Data.List.NonEmpty (toList)-import Data.List.Split (splitOn, splitOneOf)-import Data.Maybe (fromMaybe, isJust, mapMaybe)+import Data.List.Split (splitOneOf) import Hadolint.Bash import Language.Docker.Syntax 
test/Spec.hs view
@@ -1,7 +1,7 @@ import Test.HUnit hiding (Label) import Test.Hspec -import Hadolint.Formatter+import Hadolint.Formatter.TTY (formatError) import Hadolint.Rules  import Language.Docker.Parser