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 +82/−32
- hadolint.cabal +12/−4
- src/Hadolint/Formatter.hs +0/−43
- src/Hadolint/Formatter/Checkstyle.hs +91/−0
- src/Hadolint/Formatter/Codeclimate.hs +95/−0
- src/Hadolint/Formatter/Format.hs +56/−0
- src/Hadolint/Formatter/Json.hs +54/−0
- src/Hadolint/Formatter/TTY.hs +37/−0
- src/Hadolint/Rules.hs +2/−4
- test/Spec.hs +1/−1
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